GCC Code Coverage Report


Directory: ./
File: AtomicSharedPtr.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 23 23 100.0%
Functions: 24 24 100.0%
Branches: 0 0 -%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2022 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 #include <atomic>
15 #include <memory>
16 #include <mutex>
17 #include <pthread.h>
18 #include <unistd.h>
19 #include <utility>
20
21 // A mutex that lends its holder the priority of whoever is waiting.
22 //
23 // Blocking (rather than spinning) is what stops the real-time reader from
24 // livelocking, but on its own it still allows unbounded priority inversion: the
25 // normal-priority lock holder can be preempted by any of the other
26 // normal-priority threads while a real-time thread waits behind it, and on a
27 // single core that costs a scheduling round. PTHREAD_PRIO_INHERIT bounds the
28 // wait to the length of the critical section instead, by boosting the holder --
29 // Linux will promote a SCHED_OTHER owner to the waiter's real-time priority for
30 // as long as it holds the lock.
31 class PriorityInheritingMutex {
32 public:
33 16604 PriorityInheritingMutex() {
34 pthread_mutexattr_t attr;
35 16604 pthread_mutexattr_init(&attr);
36 #ifdef _POSIX_THREAD_PRIO_INHERIT
37 // Deliberately unchecked: where inheritance is unavailable (macOS
38 // accepts the call without implementing it) the mutex is still a
39 // correct mutex, just without the boost. Nothing here depends on the
40 // boost for correctness.
41 16604 pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
42 #endif
43 16604 pthread_mutex_init(&m_mutex, &attr);
44 16604 pthread_mutexattr_destroy(&attr);
45 16604 }
46 8046 ~PriorityInheritingMutex() { pthread_mutex_destroy(&m_mutex); }
47
48 PriorityInheritingMutex(const PriorityInheritingMutex&) = delete;
49 PriorityInheritingMutex& operator=(const PriorityInheritingMutex&) = delete;
50
51 6487136 void lock() { pthread_mutex_lock(&m_mutex); }
52 6487136 void unlock() { pthread_mutex_unlock(&m_mutex); }
53 bool try_lock() { return pthread_mutex_trylock(&m_mutex) == 0; }
54
55 private:
56 pthread_mutex_t m_mutex;
57 };
58
59 // A portable atomic<shared_ptr<T>>, deliberately implemented with a mutex.
60 //
61 // This used to prefer the C++20 std::atomic<std::shared_ptr<T>> specialization
62 // (P0718) where the standard library provided it, falling back to the mutex
63 // only for Apple's libc++, which does not. That preference is gone: the
64 // native specialization must not be used here.
65 //
66 // libstdc++ implements atomic<shared_ptr<T>> with an internal spinlock whose
67 // backoff calls sched_yield(). Sequence::m_seqFile is read several times per
68 // frame from the channel output thread, which runs at real-time priority, and
69 // is written/read by the frame reader thread at normal priority. On a
70 // single-core board that combination livelocks: when the normal-priority
71 // thread holds the spinlock and is preempted, the real-time thread spins on
72 // sched_yield(), which by definition only yields to threads of equal or higher
73 // priority -- so the lock holder can never be scheduled to release it. It only
74 // breaks when kernel RT throttling (sched_rt_runtime_us, ~95%) forcibly
75 // deschedules the spinner.
76 //
77 // Measured on a single-core AM335x board: roughly a third of one-minute
78 // playbacks hit a ~1 s freeze of every non-RT thread on the system, with
79 // 115,000+ sched_yield() calls from the output thread and almost no other
80 // syscalls, costing a second of frozen output and leaving the sequence
81 // permanently ~600 ms out of step with the audio. Forcing this mutex
82 // implementation took that from 5 freezes in 16 runs to 0 in 10. Multi-core
83 // boards do not show it -- the lock holder simply runs on another CPU -- which
84 // is why it looked platform-specific rather than like the scheduling bug it is.
85 //
86 // A mutex blocks on a futex instead of spinning, so the real-time thread sleeps
87 // and the lock holder runs. The cost is one uncontended lock per load(), a
88 // handful of times per frame, which does not register against a 20 ms frame.
89 // PriorityInheritingMutex above additionally bounds how long that wait can be.
90 template<typename T>
91 class AtomicSharedPtr {
92 public:
93 7639 AtomicSharedPtr() = default;
94 8965 AtomicSharedPtr(std::shared_ptr<T> p) :
95 8965 m_ptr(std::move(p)) {}
96
97 AtomicSharedPtr(const AtomicSharedPtr&) = delete;
98 AtomicSharedPtr& operator=(const AtomicSharedPtr&) = delete;
99
100 6656760 std::shared_ptr<T> load(std::memory_order = std::memory_order_seq_cst) const {
101 6656760 std::lock_guard<PriorityInheritingMutex> lk(m_lock);
102 6656760 return m_ptr;
103 6656760 }
104 137264 void store(std::shared_ptr<T> p, std::memory_order = std::memory_order_seq_cst) {
105 137264 std::lock_guard<PriorityInheritingMutex> lk(m_lock);
106 137264 m_ptr = std::move(p);
107 137264 }
108 497 AtomicSharedPtr& operator=(std::shared_ptr<T> p) {
109 497 store(std::move(p));
110 497 return *this;
111 }
112
113 private:
114 mutable PriorityInheritingMutex m_lock;
115 std::shared_ptr<T> m_ptr;
116 };
117