@@ -58,7 +58,19 @@ int ThreadedChannelOutput::Init(void) {
5858 m_inBuf = new unsigned char [m_channelCount];
5959 m_outBuf = new unsigned char [m_channelCount];
6060 }
61- StartOutputThread ();
61+ if (!StartOutputThread ()) {
62+ // Nothing can consume the buffers without a worker and the caller
63+ // discards an output whose Init() failed, so release them here.
64+ // Close() may still run on the way out; nulling keeps its delete[]
65+ // a no-op rather than a double free.
66+ if (m_useDoubleBuffer) {
67+ delete[] m_inBuf;
68+ delete[] m_outBuf;
69+ m_inBuf = nullptr ;
70+ m_outBuf = nullptr ;
71+ }
72+ return 0 ;
73+ }
6274 DumpConfig ();
6375
6476 return 1 ;
@@ -87,9 +99,19 @@ int ThreadedChannelOutput::SendData(unsigned char* channelData) {
8799 if (m_useDoubleBuffer) {
88100 std::lock_guard<std::mutex> lock (m_bufLock);
89101 memcpy (m_inBuf, channelData, m_channelCount);
90- m_dataWaiting = 1 ;
91102 } else {
92103 m_outBuf = channelData;
104+ }
105+
106+ // The flag has to be raised under m_sendLock: a store outside it can land
107+ // between the worker's predicate check and its wait(), and the notify that
108+ // follows is then delivered to nobody. The two locks are taken in
109+ // sequence, never nested, so no ordering against the worker exists to
110+ // violate. Publishing m_outBuf/m_inBuf before the lock is enough for the
111+ // worker to see them - it reads them only after acquiring m_sendLock and
112+ // observing the flag.
113+ {
114+ std::lock_guard<std::mutex> lock (m_sendLock);
93115 m_dataWaiting = 1 ;
94116 }
95117
@@ -100,12 +122,20 @@ int ThreadedChannelOutput::SendData(unsigned char* channelData) {
100122int ThreadedChannelOutput::SendOutputBuffer (void ) {
101123 LogExcess (VB_CHANNELOUT , " ChannelOutput::SendOutputBuffer()\n " );
102124
125+ // Claim the pending frame BEFORE copying it out. Clearing after the copy
126+ // opens a window where a SendData landing between the copy and the clear
127+ // has its flag wiped and its frame is never sent - fatal for a final
128+ // blanking frame, which has no successor. Claim-first means a frame
129+ // arriving mid-send leaves the flag raised and the newer data goes out on
130+ // the worker's next pass (at worst the newest frame is sent twice).
131+ {
132+ std::lock_guard<std::mutex> lock (m_sendLock);
133+ m_dataWaiting = 0 ;
134+ }
135+
103136 if (m_useDoubleBuffer) {
104137 std::lock_guard<std::mutex> lock (m_bufLock);
105138 memcpy (m_outBuf, m_inBuf, m_channelCount);
106- m_dataWaiting = 0 ;
107- } else {
108- m_dataWaiting = 0 ;
109139 }
110140
111141 RawSendData (m_outBuf);
@@ -119,7 +149,7 @@ void ThreadedChannelOutput::DumpConfig(void) {
119149 LogDebug (VB_CHANNELOUT , " Data Waiting : %u\n " , m_dataWaiting);
120150}
121151
122- int ThreadedChannelOutput::StartOutputThread (void ) {
152+ bool ThreadedChannelOutput::StartOutputThread (void ) {
123153 LogDebug (VB_CHANNELOUT , " ThreadedChannelOutput::StartOutputThread()\n " );
124154
125155 m_runThread = 1 ;
@@ -162,12 +192,18 @@ int ThreadedChannelOutput::StartOutputThread(void) {
162192 break ;
163193 }
164194 LogErr (VB_CHANNELOUT , " ERROR creating ChannelOutput thread: %s\n " , msg);
195+
196+ // Only OutputThread() ever raises m_threadIsRunning, so falling into
197+ // the wait below would spin forever.
198+ return false ;
165199 }
166200
167- while (!m_threadIsRunning)
201+ // m_runThread can only be 0 here if construction failed, which returns
202+ // above; the check keeps the wait bounded if that ever stops holding.
203+ while (m_runThread && !m_threadIsRunning)
168204 usleep (10000 );
169205
170- return 0 ;
206+ return true ;
171207}
172208
173209int ThreadedChannelOutput::StopOutputThread (void ) {
@@ -176,34 +212,38 @@ int ThreadedChannelOutput::StopOutputThread(void) {
176212 if (!m_thread.joinable ())
177213 return -1 ;
178214
179- m_runThread = 0 ;
215+ // Half of the worker's wait predicate, so it must change under m_sendLock;
216+ // a store outside the lock can be missed by a worker that is between its
217+ // predicate check and its wait(), leaving it parked until the next frame
218+ // that will never come.
219+ {
220+ std::lock_guard<std::mutex> lock (m_sendLock);
221+ m_runThread = 0 ;
222+ }
180223
181224 m_sendCond.notify_one ();
182225
226+ // Wait up to 110ms for a pending frame to be sent. The worker flushes
227+ // m_dataWaiting before it honors the stop, so this is what lets the final
228+ // blanking frame of a sequence reach the hardware.
183229 int loops = 0 ;
184- // Wait up to 110ms for data to be sent
185- while ((m_dataWaiting) &&
186- (m_threadIsRunning) &&
187- (loops++ < 11 ))
230+ while (loops++ < 11 ) {
231+ {
232+ std::lock_guard<std::mutex> lock (m_sendLock);
233+ if (!m_dataWaiting || !m_threadIsRunning)
234+ break ;
235+ }
188236 usleep (10000 );
237+ }
189238
190- // NOTE: preserved as-is from the pthread version. m_bufLock is held
191- // across the join() below. This only avoids deadlock because by this
192- // point OutputThread() is expected to be parked in
193- // m_sendCond.wait()/wait_for() on m_sendLock (not holding m_bufLock)
194- // per the 110ms settle loop above; OutputThread() does not need
195- // m_bufLock again on its way out once m_runThread is 0 (it just
196- // `continue`s straight to the top-of-loop check and exits). Do not
197- // "fix" this ordering without auditing that invariant.
198- std::unique_lock<std::mutex> lock (m_bufLock);
199-
200- if (!m_thread.joinable ()) {
201- lock.unlock ();
239+ if (!m_thread.joinable ())
202240 return -1 ;
203- }
204241
242+ // m_bufLock must NOT be held across the join(): the worker takes it while
243+ // flushing that pending frame on its way out. It guards only the contents
244+ // of m_inBuf/m_outBuf, which join() does not touch, and Close() does not
245+ // free them until after this returns.
205246 m_thread.join ();
206- lock.unlock ();
207247
208248 return 0 ;
209249}
@@ -214,30 +254,28 @@ void ThreadedChannelOutput::OutputThread(void) {
214254
215255 long long wakeTime = GetTime ();
216256
217- // unique_lock, not locked yet (mirrors the pthread version which did
218- // not hold m_sendLock outside the loop body either); locked/unlocked
219- // explicitly at the same points the old pthread_mutex_lock/unlock
220- // calls were, rather than relying on scope-based RAII, so the
221- // lock/unlock coverage stays a 1:1 match with the original.
257+ // Deferred: m_sendLock is held only while the predicate state is read or
258+ // written, never across SendOutputBuffer()/WaitTimedOut(), which can block
259+ // on hardware for as long as they like.
222260 std::unique_lock<std::mutex> sendLock (m_sendLock, std::defer_lock);
223261
262+ sendLock.lock ();
224263 m_threadIsRunning = 1 ;
264+ sendLock.unlock ();
225265 LogDebug (VB_CHANNELOUT , " ThreadedChannelOutput thread started\n " );
226266
227- while (m_runThread) {
267+ // m_dataWaiting and m_runThread are both owned by m_sendLock, so this
268+ // closes the window SendData's store-then-notify used to leave open.
269+ auto ready = [this ]() { return m_dataWaiting || !m_runThread; };
270+
271+ while (true ) {
228272 // Wait for more data
229273 sendLock.lock ();
230274 long long nowTime = GetTime ();
231275 LogExcess (VB_CHANNELOUT , " ThreadedChannelOutput thread: sent: %lld, elapsed: %lld\n " ,
232276 nowTime, nowTime - wakeTime);
233277
234- if (m_useDoubleBuffer)
235- m_bufLock.lock ();
236-
237278 if (m_dataWaiting || m_maxWait) {
238- if (m_useDoubleBuffer)
239- m_bufLock.unlock ();
240-
241279 // Old code computed an absolute CLOCK_REALTIME deadline
242280 // (now + duration) via gettimeofday()/timespec math for
243281 // pthread_cond_timedwait(). That duration was:
@@ -247,44 +285,41 @@ void ThreadedChannelOutput::OutputThread(void) {
247285 // timespec/overflow-carry arithmetic is no longer needed.
248286 std::chrono::milliseconds waitDuration (m_maxWait ? m_maxWait : 200 );
249287
250- // No predicate is passed (matches pthread_cond_timedwait,
251- // which also does not loop internally): a spurious or timed
252- // wakeup here just falls through to the checks below, and
253- // the outer while(m_runThread) loop is what re-evaluates
254- // state on the next pass, exactly as before.
255- m_sendCond.wait_for (sendLock, waitDuration);
288+ // The predicate overload computes the deadline once, so spurious
289+ // wakeups do not extend it and m_maxWait keeps its meaning: the
290+ // interval at which a subclass wants WaitTimedOut() called.
291+ m_sendCond.wait_for (sendLock, waitDuration, ready);
256292 } else {
257- if (m_useDoubleBuffer)
258- m_bufLock.unlock ();
259-
260- // No predicate here either, matching pthread_cond_wait().
261- m_sendCond.wait (sendLock);
293+ m_sendCond.wait (sendLock, ready);
262294 }
263295
264- sendLock.unlock ();
296+ // Sampled together under the lock so the two decisions below cannot
297+ // see a half-updated state.
298+ bool haveData = m_dataWaiting;
299+ bool keepRunning = m_runThread;
265300
266- if (!m_runThread)
267- continue ;
301+ sendLock.unlock ();
268302
269303 wakeTime = GetTime ();
270304 LogExcess (VB_CHANNELOUT , " ThreadedChannelOutput thread: woke: %lld\n " , wakeTime);
271305
272- // See if there is any data waiting to process or if we timed out
273- if (m_useDoubleBuffer)
274- m_bufLock.lock ();
275-
276- if (m_dataWaiting) {
277- if (m_useDoubleBuffer)
278- m_bufLock.unlock ();
279-
306+ // A pending frame is flushed even once a stop has been requested: the
307+ // last frame of a sequence is the blanking frame and StopOutputThread
308+ // follows it immediately, so honoring the stop first leaves the lights
309+ // lit. Reaching here with neither data nor a stop means the wait
310+ // timed out, which is the only thing WaitTimedOut() is for.
311+ if (haveData) {
280312 SendOutputBuffer ();
281- } else {
282- if (m_useDoubleBuffer)
283- m_bufLock.unlock ();
313+ } else if (keepRunning) {
284314 WaitTimedOut ();
285315 }
316+
317+ if (!keepRunning)
318+ break ;
286319 }
287320
288321 LogDebug (VB_CHANNELOUT , " ThreadedChannelOutput thread complete\n " );
322+ sendLock.lock ();
289323 m_threadIsRunning = 0 ;
324+ sendLock.unlock ();
290325}
0 commit comments