Coverage Report

Created: 2026-07-25 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/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
5.43k
  {
85
5.43k
    checkAndRethrowException();
86
87
5.43k
    m_lockState.store( false );
88
5.43k
  }
89
90
  virtual void unlock_nothrow()
91
1.40k
  {
92
1.40k
    m_lockState.store( false );
93
1.40k
  }
94
95
  virtual void lock()
96
22.0k
  {
97
22.0k
    checkAndRethrowException();
98
99
22.0k
    m_lockState.store( true );
100
22.0k
  }
101
102
  bool isBlocked() const
103
6.83M
  {
104
6.83M
    checkAndRethrowException();
105
106
6.83M
    return m_lockState;
107
6.83M
  }
108
109
  enum State
110
  {
111
    unlocked,
112
    locked,
113
    error
114
  };
115
116
  State getState() const
117
713
  {
118
713
    if( m_hasException )
119
0
      return error;
120
121
713
    if( m_lockState )
122
713
      return locked;
123
0
    return unlocked;
124
713
  }
125
126
  virtual void setException( std::exception_ptr e )
127
22.6k
  {
128
22.6k
    std::lock_guard<std::mutex> l( s_exceptionLock );
129
22.6k
    if( m_hasException )
130
9.91k
    {
131
9.91k
      CHECK_FATAL( m_exception == nullptr, "no exception currently stored, but flag is set" );
132
      // exception is already set -> no-op
133
9.91k
      return;
134
9.91k
    }
135
12.7k
    m_exception    = e;
136
12.7k
    m_hasException = true;
137
12.7k
  }
138
139
  virtual void clearException()
140
4.28k
  {
141
4.28k
    if( m_hasException )
142
2.81k
    {
143
2.81k
      std::lock_guard<std::mutex> l( s_exceptionLock );
144
2.81k
      m_hasException = false;
145
2.81k
      m_exception    = nullptr;
146
2.81k
    }
147
4.28k
  }
148
149
  const std::exception_ptr getException() const
150
3.46k
  {
151
3.46k
    if( !m_hasException )
152
0
    {
153
0
      return nullptr;
154
0
    }
155
156
3.46k
    std::lock_guard<std::mutex> l( s_exceptionLock );
157
3.46k
    return m_exception;
158
3.46k
  }
159
160
664k
  bool hasException() const { return m_hasException; }
161
162
  inline void checkAndRethrowException() const
163
6.85M
  {
164
6.85M
    if LIKELY( !m_hasException )
165
6.85M
    {
166
6.85M
      return;
167
6.85M
    }
168
169
7.10k
    std::lock_guard<std::mutex> l( s_exceptionLock );
170
7.10k
    if( m_hasException )
171
7.49k
    {
172
7.49k
      CHECK_FATAL( m_exception == nullptr, "no exception currently stored, but flag is set" );
173
7.49k
      std::rethrow_exception( m_exception );
174
7.49k
    }
175
7.10k
  }
176
177
13.0k
  Barrier()  = default;
178
15.1k
  virtual ~Barrier() = default;
179
2.14k
  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
716
  {
194
716
    std::lock_guard<std::mutex> l( m_lock );
195
716
    Barrier::unlock();
196
716
    m_cond.notify_all();
197
716
  }
198
199
  void lock() override
200
1.42k
  {
201
1.42k
    std::lock_guard<std::mutex> l( m_lock );
202
1.42k
    Barrier::lock();
203
1.42k
  }
204
205
  void wait() const
206
713
  {
207
713
    std::unique_lock<std::mutex> l( m_lock );
208
713
    if( Barrier::isBlocked() )
209
686
    {
210
1.37k
      m_cond.wait( l, [this] { return !Barrier::isBlocked(); } );
211
686
    }
212
713
  }
213
214
  void setException( std::exception_ptr e ) override
215
2.13k
  {
216
2.13k
    std::lock_guard<std::mutex> l( m_lock );
217
2.13k
    Barrier::setException( e );
218
2.13k
    m_cond.notify_all();
219
2.13k
  }
220
221
  void clearException() override
222
1.42k
  {
223
1.42k
    std::lock_guard<std::mutex> l( m_lock );
224
1.42k
    Barrier::clearException();
225
1.42k
  }
226
227
1.42k
  BlockingBarrier()  = default;
228
1.42k
  ~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
10.6k
  {
240
10.6k
    std::lock_guard<std::mutex> l( m_lock );
241
10.6k
    m_done.lock();
242
10.6k
    return ++m_count;
243
10.6k
  }
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
10.6k
  {
261
10.6k
    std::unique_lock<std::mutex> l( m_lock );
262
10.6k
    const int new_count = --m_count;
263
10.6k
    if( new_count <= 0 )
264
1.40k
    {
265
1.40k
      CHECK_WARN( new_count < 0, "WaitCounter is negative, this is probably a bug." );
266
1.40k
      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
1.40k
      m_done.unlock_nothrow();
268
1.40k
    }
269
10.6k
    l.unlock(); // unlock mutex after done-barrier to prevent race between barrier and counter
270
10.6k
    return new_count;
271
10.6k
  }
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
2.13k
  {
289
2.13k
    std::unique_lock<std::mutex> l( m_lock );
290
4.84k
    m_cond.wait( l, [this] { return m_count <= 0; } );
291
2.13k
  }
292
293
  void setException( std::exception_ptr e )
294
10.5k
  {
295
10.5k
    std::lock_guard<std::mutex> l( m_lock );
296
10.5k
    m_done.setException( e );
297
10.5k
    m_cond.notify_all();
298
10.5k
  }
299
300
  void clearException()
301
2.14k
  {
302
2.14k
    std::lock_guard<std::mutex> l( m_lock );
303
2.14k
    m_done.clearException();
304
2.14k
  }
305
306
663k
  bool                     hasException() const { return m_done.hasException(); }
307
3.46k
  const std::exception_ptr getException() const { return m_done.getException(); }
308
309
2.14k
  WaitCounter() = default;
310
2.14k
  ~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
686
  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
935
      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
905k
      Iterator() = default;
379
30.8k
      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
96.5M
      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
95.7M
      Slot& operator*() { return *m_slot; }
390
391
2.11M
      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
935
    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
30.8k
    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
935
    PoolPause( size_t numThreads ) : m_nrThreads( numThreads ){};
422
13.4k
    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
935
    ~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
10.6k
  {
449
10.6k
    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
10.6k
    else
458
10.6k
    {
459
10.6k
      checkAndThrowThreadPoolException();
460
10.6k
    }
461
462
10.6k
    while( true )
463
10.6k
    {
464
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
465
      std::unique_lock<std::mutex> l( m_nextFillSlotMutex );
466
#endif
467
10.6k
      CHECKD( !m_nextFillSlot.isValid(), "Next fill slot iterator should always be valid" );
468
10.6k
      const auto startIt = m_nextFillSlot;
469
470
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
471
      l.unlock();
472
#endif
473
474
10.6k
      bool first = true;
475
10.6k
      for( auto it = startIt; it != startIt || first; it.incWrap() )
476
10.6k
      {
477
10.6k
        first = false;
478
479
10.6k
        auto& t = *it;
480
10.6k
        auto expected = FREE;
481
10.6k
        if( t.state.load( std::memory_order_relaxed ) == FREE && t.state.compare_exchange_strong( expected, PREPARING ) )
482
10.6k
        {
483
10.6k
          if( counter )
484
10.6k
          {
485
10.6k
            counter->operator++();
486
10.6k
          }
487
488
10.6k
          t.func       = (TaskFunc)func;
489
10.6k
          t.readyCheck = (TaskFunc)readyCheck;
490
10.6k
          t.param      = param;
491
10.6k
          t.done       = done;
492
10.6k
          t.counter    = counter;
493
10.6k
          t.barriers   = std::move( barriers );
494
#if THREAD_POOL_TASK_NAMES
495
          t.taskName   = std::move( taskName );
496
#endif
497
10.6k
          auto poolPauseLock( m_poolPause.acquireLock() );
498
10.6k
          t.state = WAITING;
499
500
10.6k
          m_poolPause.unpauseIfPaused( std::move( poolPauseLock ) );
501
502
#if THREAD_POOL_ADD_TASK_THREAD_SAFE
503
          l.lock();
504
#endif
505
10.6k
          m_nextFillSlot.incWrap();
506
10.6k
          return true;
507
10.6k
        }
508
10.6k
      }
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
10.6k
  }
517
518
  bool processTasksOnMainThread();
519
  void checkAndThrowThreadPoolException();
520
521
  void shutdown( bool block );
522
  void waitForThreads();
523
524
6.12k
  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