Coverage Report

Created: 2026-07-16 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/work/vvdec/source/Lib/Utilities/ThreadPool.h
Line
Count
Source
1
/* -----------------------------------------------------------------------------
2
The copyright in this software is being made available under the Clear BSD
3
License, included below. No patent rights, trademark rights and/or 
4
other Intellectual Property Rights other than the copyrights concerning 
5
the Software are granted under this license.
6
7
The Clear BSD License
8
9
Copyright (c) 2018-2026, Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. & The VVdeC Authors.
10
All rights reserved.
11
12
Redistribution and use in source and binary forms, with or without modification,
13
are permitted (subject to the limitations in the disclaimer below) provided that
14
the following conditions are met:
15
16
     * Redistributions of source code must retain the above copyright notice,
17
     this list of conditions and the following disclaimer.
18
19
     * Redistributions in binary form must reproduce the above copyright
20
     notice, this list of conditions and the following disclaimer in the
21
     documentation and/or other materials provided with the distribution.
22
23
     * Neither the name of the copyright holder nor the names of its
24
     contributors may be used to endorse or promote products derived from this
25
     software without specific prior written permission.
26
27
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
28
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
29
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
31
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
32
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
33
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
34
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
35
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
36
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
37
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38
POSSIBILITY OF SUCH DAMAGE.
39
40
41
------------------------------------------------------------------------------------------- */
42
43
#pragma once
44
45
#include <thread>
46
#include <mutex>
47
#include <condition_variable>
48
#include <atomic>
49
#include <exception>
50
#include <array>
51
52
#include "CommonLib/CommonDef.h"
53
54
namespace vvdec
55
{
56
57
#ifdef TRACE_ENABLE_ITT
58
static __itt_domain* itt_domain_thrd = __itt_domain_create( "Threading" );
59
60
static __itt_string_handle* itt_handle_TPspinWait = __itt_string_handle_create( "Spin_Wait" );
61
static __itt_string_handle* itt_handle_TPblocked  = __itt_string_handle_create( "Blocked" );
62
static __itt_string_handle* itt_handle_TPaddTask  = __itt_string_handle_create( "Add_Task" );
63
64
// static long itt_TP_blocked = 1;
65
#endif   // TRACE_ENABLE_ITT
66
67
#define THREAD_POOL_ADD_TASK_THREAD_SAFE 0   // enable this if tasks need to be added from mutliple threads
68
#define THREAD_POOL_TASK_NAMES           0   // simplify debugging of thread pool deadlocks
69
70
#if THREAD_POOL_TASK_NAMES
71
#  define TP_TASK_NAME_ARG( ... ) __VA_ARGS__ ,
72
#else
73
#  define TP_TASK_NAME_ARG( ... )
74
#endif
75
76
77
// ---------------------------------------------------------------------------
78
// Synchronization tools
79
// ---------------------------------------------------------------------------
80
81
struct Barrier
82
{
83
  virtual void unlock()
84
0
  {
85
0
    checkAndRethrowException();
86
87
0
    m_lockState.store( false );
88
0
  }
89
90
  virtual void unlock_nothrow()
91
0
  {
92
0
    m_lockState.store( false );
93
0
  }
94
95
  virtual void lock()
96
0
  {
97
0
    checkAndRethrowException();
98
99
0
    m_lockState.store( true );
100
0
  }
101
102
  bool isBlocked() const
103
0
  {
104
0
    checkAndRethrowException();
105
106
0
    return m_lockState;
107
0
  }
108
109
  enum State
110
  {
111
    unlocked,
112
    locked,
113
    error
114
  };
115
116
  State getState() const
117
0
  {
118
0
    if( m_hasException )
119
0
      return error;
120
121
0
    if( m_lockState )
122
0
      return locked;
123
0
    return unlocked;
124
0
  }
125
126
  virtual void setException( std::exception_ptr e )
127
0
  {
128
0
    std::lock_guard<std::mutex> l( s_exceptionLock );
129
0
    if( m_hasException )
130
0
    {
131
0
      CHECK_FATAL( m_exception == nullptr, "no exception currently stored, but flag is set" );
132
      // exception is already set -> no-op
133
0
      return;
134
0
    }
135
0
    m_exception    = e;
136
0
    m_hasException = true;
137
0
  }
138
139
  virtual void clearException()
140
0
  {
141
0
    if( m_hasException )
142
0
    {
143
0
      std::lock_guard<std::mutex> l( s_exceptionLock );
144
0
      m_hasException = false;
145
0
      m_exception    = nullptr;
146
0
    }
147
0
  }
148
149
  const std::exception_ptr getException() const
150
0
  {
151
0
    if( !m_hasException )
152
0
    {
153
0
      return nullptr;
154
0
    }
155
156
0
    std::lock_guard<std::mutex> l( s_exceptionLock );
157
0
    return m_exception;
158
0
  }
159
160
0
  bool hasException() const { return m_hasException; }
161
162
  inline void checkAndRethrowException() const
163
0
  {
164
0
    if LIKELY( !m_hasException )
165
0
    {
166
0
      return;
167
0
    }
168
169
0
    std::lock_guard<std::mutex> l( s_exceptionLock );
170
0
    if( m_hasException )
171
0
    {
172
0
      CHECK_FATAL( m_exception == nullptr, "no exception currently stored, but flag is set" );
173
0
      std::rethrow_exception( m_exception );
174
0
    }
175
0
  }
176
177
356
  Barrier()  = default;
178
356
  virtual ~Barrier() = default;
179
0
  explicit Barrier( bool locked ) : m_lockState( locked ) {}
180
  CLASS_COPY_MOVE_DELETE( Barrier )
181
182
private:
183
  std::atomic_bool   m_lockState{ true };
184
  std::atomic_bool   m_hasException{ false };
185
  std::exception_ptr m_exception;
186
  static std::mutex  s_exceptionLock;   // we use one shared mutex for all barriers here. It is only involved, when exceptions actually happen, so there should
187
                                        // be no contention during normal operations
188
};
189
190
struct BlockingBarrier: public Barrier
191
{
192
  void unlock() override
193
0
  {
194
0
    std::lock_guard<std::mutex> l( m_lock );
195
0
    Barrier::unlock();
196
0
    m_cond.notify_all();
197
0
  }
198
199
  void lock() override
200
0
  {
201
0
    std::lock_guard<std::mutex> l( m_lock );
202
0
    Barrier::lock();
203
0
  }
204
205
  void wait() const
206
0
  {
207
0
    std::unique_lock<std::mutex> l( m_lock );
208
0
    if( Barrier::isBlocked() )
209
0
    {
210
0
      m_cond.wait( l, [this] { return !Barrier::isBlocked(); } );
211
0
    }
212
0
  }
213
214
  void setException( std::exception_ptr e ) override
215
0
  {
216
0
    std::lock_guard<std::mutex> l( m_lock );
217
0
    Barrier::setException( e );
218
0
    m_cond.notify_all();
219
0
  }
220
221
  void clearException() override
222
0
  {
223
0
    std::lock_guard<std::mutex> l( m_lock );
224
0
    Barrier::clearException();
225
0
  }
226
227
0
  BlockingBarrier()  = default;
228
0
  ~BlockingBarrier() { std::lock_guard<std::mutex> l( m_lock ); }   // ensure all threads have unlocked the mutex, when we start destruction
229
  CLASS_COPY_MOVE_DELETE( BlockingBarrier )
230
231
private:
232
  mutable std::condition_variable m_cond;
233
  mutable std::mutex              m_lock;
234
};
235
236
struct WaitCounter
237
{
238
  int operator++()
239
0
  {
240
0
    std::lock_guard<std::mutex> l( m_lock );
241
0
    m_done.lock();
242
0
    return ++m_count;
243
0
  }
244
245
  int operator--()
246
0
  {
247
0
    std::unique_lock<std::mutex> l( m_lock );
248
0
    const int new_count = --m_count;
249
0
    if( new_count <= 0 )
250
0
    {
251
0
      CHECK( new_count < 0, "WaitCounter is negative, this is probably a bug." );
252
0
      m_cond.notify_all();   // we can notify before unlocking the barrier, because wait() and wait_nothrow() wait for m_count and not for m_done
253
0
      m_done.unlock();
254
0
    }
255
0
    l.unlock(); // unlock mutex after done-barrier to prevent race between barrier and counter
256
0
    return new_count;
257
0
  }
258
259
  int decrement_nothrow()
260
0
  {
261
0
    std::unique_lock<std::mutex> l( m_lock );
262
0
    const int new_count = --m_count;
263
0
    if( new_count <= 0 )
264
0
    {
265
0
      CHECK_WARN( new_count < 0, "WaitCounter is negative, this is probably a bug." );
266
0
      m_cond.notify_all();   // we can notify before unlocking the barrier, because wait() and wait_nothrow() wait for m_count and not for m_done
267
0
      m_done.unlock_nothrow();
268
0
    }
269
0
    l.unlock(); // unlock mutex after done-barrier to prevent race between barrier and counter
270
0
    return new_count;
271
0
  }
272
273
  bool isBlocked() const
274
0
  {
275
0
    std::lock_guard<std::mutex> l( m_lock );
276
0
    m_done.checkAndRethrowException();
277
0
    return m_count > 0;
278
0
  }
279
280
  void wait() const
281
0
  {
282
0
    std::unique_lock<std::mutex> l( m_lock );
283
0
    m_cond.wait( l, [this] { return m_count <= 0 || m_done.hasException(); } );
284
0
    m_done.checkAndRethrowException();
285
0
  }
286
287
  void wait_nothrow() const
288
0
  {
289
0
    std::unique_lock<std::mutex> l( m_lock );
290
0
    m_cond.wait( l, [this] { return m_count <= 0; } );
291
0
  }
292
293
  void setException( std::exception_ptr e )
294
0
  {
295
0
    std::lock_guard<std::mutex> l( m_lock );
296
0
    m_done.setException( e );
297
0
    m_cond.notify_all();
298
0
  }
299
300
  void clearException()
301
0
  {
302
0
    std::lock_guard<std::mutex> l( m_lock );
303
0
    m_done.clearException();
304
0
  }
305
306
0
  bool                     hasException() const { return m_done.hasException(); }
307
0
  const std::exception_ptr getException() const { return m_done.getException(); }
308
309
0
  WaitCounter() = default;
310
0
  ~WaitCounter() { std::lock_guard<std::mutex> l( m_lock ); }   // ensure all threads have unlocked the mutex, when we start destruction
311
  CLASS_COPY_MOVE_DELETE( WaitCounter )
312
313
0
  const Barrier* donePtr() const { return &m_done; }
314
315
private:
316
  mutable std::condition_variable m_cond;   // We can't use a BlockingBarrier but need a separate condition variable here,
317
                                            // so we can wait for m_count to reach zero (all tasks have been cleared from
318
                                            // the thread pool) even if the exception flag is set.
319
  mutable std::mutex              m_lock;
320
  int                             m_count = 0;
321
  Barrier                         m_done{ false };
322
};
323
324
// ---------------------------------------------------------------------------
325
// Thread Pool
326
// ---------------------------------------------------------------------------
327
328
using CBarrierVec = std::vector<const Barrier*>;
329
330
class ThreadPool
331
{
332
  typedef enum
333
  {
334
    FREE = 0,
335
    PREPARING,
336
    WAITING,
337
    RUNNING
338
  } TaskState;
339
340
  using TaskFunc = bool ( * )( int, void * );
341
342
  struct Slot
343
  {
344
    TaskFunc               func      { nullptr };
345
    TaskFunc               readyCheck{ nullptr };
346
    void*                  param     { nullptr };
347
    WaitCounter*           counter   { nullptr };
348
    Barrier*               done      { nullptr };
349
    CBarrierVec            barriers;
350
    std::atomic<TaskState> state     { FREE };
351
#if THREAD_POOL_TASK_NAMES
352
    std::string            taskName;
353
#endif
354
  };
355
356
  class ChunkedTaskQueue
357
  {
358
    constexpr static int ChunkSize = 128;
359
360
    class Chunk
361
    {
362
      std::array<Slot, ChunkSize> m_slots;
363
      std::atomic<Chunk*>         m_next{ nullptr };
364
      Chunk&                      m_firstChunk;
365
366
356
      Chunk( Chunk* firstPtr ) : m_firstChunk{ *firstPtr } {}
367
368
      friend class ChunkedTaskQueue;
369
    };
370
371
  public:
372
    class Iterator
373
    {
374
      Slot*  m_slot  = nullptr;
375
      Chunk* m_chunk = nullptr;
376
377
    public:
378
81.0k
      Iterator() = default;
379
11.7k
      Iterator( Slot* slot, Chunk* chunk ) : m_slot( slot ), m_chunk( chunk ) {}
380
381
      Iterator& operator++();
382
383
      // increment iterator and wrap around, if end is reached
384
      Iterator& incWrap();
385
386
0
      bool operator==( const Iterator& rhs ) const { return m_slot == rhs.m_slot; } // don't need to compare m_chunk, because m_slot is a pointer
387
4.26M
      bool operator!=( const Iterator& rhs ) const { return m_slot != rhs.m_slot; } // don't need to compare m_chunk, because m_slot is a pointer
388
389
4.23M
      Slot& operator*() { return *m_slot; }
390
391
161k
      bool isValid() const { return m_slot != nullptr && m_chunk != nullptr; }
392
393
      using iterator_category = std::forward_iterator_tag;
394
      using value_type        = Slot;
395
      using pointer           = Slot*;
396
      using reference         = Slot&;
397
      using difference_type   = ptrdiff_t;
398
    };
399
400
356
    ChunkedTaskQueue() = default;
401
    ~ChunkedTaskQueue();
402
    CLASS_COPY_MOVE_DELETE( ChunkedTaskQueue )
403
404
    // grow the queue by adding a chunk and return an iterator to the first new task-slot
405
    Iterator grow();
406
407
11.7k
    Iterator begin() { return Iterator{ &m_firstChunk.m_slots.front(), &m_firstChunk }; }
408
0
    Iterator end()   { return Iterator{ nullptr, nullptr }; }
409
410
  private:
411
    Chunk  m_firstChunk{ &m_firstChunk };
412
    Chunk* m_lastChunk = &m_firstChunk;
413
414
    std::mutex m_resizeMutex;
415
  };
416
417
private:
418
  class PoolPause
419
  {
420
  public:
421
356
    PoolPause( size_t numThreads ) : m_nrThreads( numThreads ){};
422
1.06k
    auto acquireLock() { return std::unique_lock<std::mutex>( m_allThreadsWaitingMutex ); }
423
    void unpauseIfPaused( std::unique_lock<std::mutex> lockOwnership );
424
    template<typename Predicate>
425
    bool pauseIfAllOtherThreadsWaiting( Predicate predicate );
426
356
    ~PoolPause() { unpauseIfPaused( acquireLock() ); }
427
428
    std::atomic_uint m_waitingForLockThreads{ 0 };
429
430
  private:
431
    std::mutex              m_allThreadsWaitingMutex;
432
    std::condition_variable m_allThreadsWaitingCV;
433
    bool                    m_allThreadsWaiting{ false };
434
    size_t                  m_nrThreads{};
435
  };
436
437
public:
438
  ThreadPool( int numThreads = 1, const char *threadPoolName = nullptr );
439
  ~ThreadPool();
440
441
  bool addBarrierTask( TP_TASK_NAME_ARG( std::string&& taskName )
442
                       bool       ( *func )( int, void* ),
443
                       void*         param,
444
                       WaitCounter*  counter                    = nullptr,
445
                       Barrier*      done                       = nullptr,
446
                       CBarrierVec&& barriers                   = {},
447
                       bool       ( *readyCheck )( int, void* ) = nullptr )
448
0
  {
449
0
    if( m_threads.empty() )
450
0
    {
451
      // in the single threaded case try to exectute the task directly
452
0
      if( bypassTaskQueue( (TaskFunc)func, param, counter, done, barriers, (TaskFunc)readyCheck ) )
453
0
      {
454
0
        return true;
455
0
      }
456
0
    }
457
0
    else
458
0
    {
459
0
      checkAndThrowThreadPoolException();
460
0
    }
461
462
0
    while( true )
463
0
    {
464
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
465
      std::unique_lock<std::mutex> l( m_nextFillSlotMutex );
466
#endif
467
0
      CHECKD( !m_nextFillSlot.isValid(), "Next fill slot iterator should always be valid" );
468
0
      const auto startIt = m_nextFillSlot;
469
470
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
471
      l.unlock();
472
#endif
473
474
0
      bool first = true;
475
0
      for( auto it = startIt; it != startIt || first; it.incWrap() )
476
0
      {
477
0
        first = false;
478
479
0
        auto& t = *it;
480
0
        auto expected = FREE;
481
0
        if( t.state.load( std::memory_order_relaxed ) == FREE && t.state.compare_exchange_strong( expected, PREPARING ) )
482
0
        {
483
0
          if( counter )
484
0
          {
485
0
            counter->operator++();
486
0
          }
487
488
0
          t.func       = (TaskFunc)func;
489
0
          t.readyCheck = (TaskFunc)readyCheck;
490
0
          t.param      = param;
491
0
          t.done       = done;
492
0
          t.counter    = counter;
493
0
          t.barriers   = std::move( barriers );
494
#if THREAD_POOL_TASK_NAMES
495
          t.taskName   = std::move( taskName );
496
#endif
497
0
          auto poolPauseLock( m_poolPause.acquireLock() );
498
0
          t.state = WAITING;
499
500
0
          m_poolPause.unpauseIfPaused( std::move( poolPauseLock ) );
501
502
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
503
          l.lock();
504
#endif
505
0
          m_nextFillSlot.incWrap();
506
0
          return true;
507
0
        }
508
0
      }
509
510
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
511
      l.lock();
512
#endif
513
0
      m_nextFillSlot = m_tasks.grow();
514
0
    }
515
0
    return false;
516
0
  }
517
518
  bool processTasksOnMainThread();
519
  void checkAndThrowThreadPoolException();
520
521
  void shutdown( bool block );
522
  void waitForThreads();
523
524
712
  int numThreads() const { return (int)m_threads.size(); }
525
526
private:
527
  using TaskIterator = ChunkedTaskQueue::Iterator;
528
  struct TaskException;
529
530
  // members
531
  std::string              m_poolName;
532
  std::atomic_bool         m_exitThreads{ false };
533
  std::vector<std::thread> m_threads;
534
  ChunkedTaskQueue         m_tasks;
535
  TaskIterator             m_nextFillSlot = m_tasks.begin();
536
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
537
  std::mutex               m_nextFillSlotMutex;
538
#endif
539
  std::mutex               m_idleMutex;
540
  PoolPause                m_poolPause;
541
542
  std::atomic_bool         m_exceptionFlag{ false };
543
  std::exception_ptr       m_threadPoolException;
544
545
546
  // internal functions
547
  void         threadProc     ( int threadId );
548
  static bool  checkTaskReady ( int threadId, CBarrierVec& barriers, TaskFunc readyCheck, void* taskParam );
549
  TaskIterator findNextTask   ( int threadId, TaskIterator startSearch );
550
  static bool  processTask    ( int threadId, Slot& task );
551
  bool         bypassTaskQueue( TaskFunc func, void* param, WaitCounter* counter, Barrier* done, CBarrierVec& barriers, TaskFunc readyCheck );
552
  static void  handleTaskException( const std::exception_ptr e, Barrier* done, WaitCounter* counter, std::atomic<TaskState>* slot_state );
553
#if THREAD_POOL_TASK_NAMES
554
  void         printWaitingTasks();
555
#endif
556
};
557
558
}   // namespace vvdec