Skip to content

Commit c08264b

Browse files
ThreadPool: tighten task contracts and validation
Clarify cancellation, queued-task ownership, duplicate enqueue, and worker-thread wait/shutdown restrictions in the thread pool documentation. Reject NaN task priorities by falling back to the default priority, and preserve the full previous affinity mask returned by SetCurrentThreadAffinity().
1 parent b1a5596 commit c08264b

4 files changed

Lines changed: 76 additions & 9 deletions

File tree

Common/interface/ThreadPool.h

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,16 @@ DILIGENT_BEGIN_INTERFACE(IAsyncTask, IObject)
9090
VIRTUAL ASYNC_TASK_STATUS METHOD(Run)(THIS_
9191
Uint32 ThreadId) PURE;
9292

93-
/// Cancel the task, if possible.
94-
95-
/// If the task is running, the task implementation should
96-
/// abort the task execution, if possible.
93+
/// Requests task cancellation, if possible.
94+
95+
/// This is a cooperative cancellation request. If the task is running, the
96+
/// task implementation should observe the request and abort execution, if
97+
/// possible. Calling this method does not remove the task from a thread
98+
/// pool queue and does not guarantee that a task in the
99+
/// Diligent::ASYNC_TASK_STATUS_NOT_STARTED state immediately transitions to
100+
/// Diligent::ASYNC_TASK_STATUS_CANCELLED.
101+
///
102+
/// To cancel a queued task that has not started, use IThreadPool::RemoveTask().
97103
VIRTUAL void METHOD(Cancel)(THIS) PURE;
98104

99105
/// Sets the task status, see Diligent::ASYNC_TASK_STATUS.
@@ -103,11 +109,14 @@ DILIGENT_BEGIN_INTERFACE(IAsyncTask, IObject)
103109
/// Gets the task status, see Diligent::ASYNC_TASK_STATUS.
104110
VIRTUAL ASYNC_TASK_STATUS METHOD(GetStatus)(THIS) CONST PURE;
105111

106-
/// Sets the task priorirty.
112+
/// Sets the task priority.
113+
114+
/// NaN priority is invalid. Implementations will report an error and use
115+
/// the default priority 0 instead.
107116
VIRTUAL void METHOD(SetPriority)(THIS_
108117
float fPriority) PURE;
109118

110-
/// Returns the task priorirty.
119+
/// Returns the task priority.
111120
VIRTUAL float METHOD(GetPriority)(THIS) CONST PURE;
112121

113122
/// Checks if the task is finished (i.e. cancelled or complete).
@@ -175,6 +184,9 @@ DILIGENT_BEGIN_INTERFACE(IThreadPool, IObject)
175184
///
176185
/// Thread pool will keep a strong reference to the task,
177186
/// so an application is free to release it after enqueuing.
187+
/// A task object represents one scheduled execution and must not be
188+
/// enqueued again until that execution has finished or the task has been
189+
/// removed from the queue.
178190
///
179191
/// The thread pool does not keep strong references to prerequisite tasks.
180192
/// Prerequisites are tracked weakly; if a prerequisite object expires before
@@ -220,6 +232,11 @@ DILIGENT_BEGIN_INTERFACE(IThreadPool, IObject)
220232
///
221233
/// \return true if the task was successfully removed from the queue,
222234
/// and false otherwise.
235+
///
236+
/// If the task is found in the queue, it is marked as
237+
/// Diligent::ASYNC_TASK_STATUS_CANCELLED. This unblocks callers waiting in
238+
/// IAsyncTask::WaitForCompletion(). Running tasks are not removed; call
239+
/// IAsyncTask::Cancel() to request cooperative cancellation of a running task.
223240
VIRTUAL bool METHOD(RemoveTask)(THIS_
224241
IAsyncTask* pTask) PURE;
225242

@@ -230,6 +247,13 @@ DILIGENT_BEGIN_INTERFACE(IThreadPool, IObject)
230247
/// tasks in the quque are finished and the queue is empty.
231248
/// An application is responsible to make sure that all tasks
232249
/// will finish eventually.
250+
///
251+
/// \warning This method must not be called from a task running in this
252+
/// thread pool: the task is counted as running until it returns,
253+
/// while this method waits for all running tasks to finish.
254+
///
255+
/// \warning Deadlock may also occur if all worker threads block waiting for
256+
/// work that requires those same worker threads to make progress.
233257
VIRTUAL void METHOD(WaitForAllTasks)(THIS) PURE;
234258

235259

@@ -247,6 +271,9 @@ DILIGENT_BEGIN_INTERFACE(IThreadPool, IObject)
247271
/// threads exit, so this is a graceful drain-and-stop operation rather than
248272
/// an immediate cancellation of queued work.
249273
///
274+
/// \warning This method must not be called from a worker thread of this
275+
/// pool because it joins all worker threads, including the caller.
276+
///
250277
/// Enqueuing tasks after calling this method is an error.
251278
VIRTUAL void METHOD(StopThreads)(THIS) PURE;
252279

Common/interface/ThreadPool.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "ThreadPool.h"
3333

3434
#include <atomic>
35+
#include <cmath>
3536
#include <condition_variable>
3637
#include <functional>
3738
#include <mutex>
@@ -87,7 +88,7 @@ class AsyncTaskBase : public ObjectBase<IAsyncTask>
8788
explicit AsyncTaskBase(IReferenceCounters* pRefCounters,
8889
float fPriority = 0) noexcept :
8990
TBase{pRefCounters},
90-
m_fPriority{fPriority}
91+
m_fPriority{ValidatePriority(fPriority)}
9192
{
9293
}
9394
virtual ~AsyncTaskBase() = 0;
@@ -156,7 +157,7 @@ class AsyncTaskBase : public ObjectBase<IAsyncTask>
156157

157158
virtual void DILIGENT_CALL_TYPE SetPriority(float fPriority) override final
158159
{
159-
m_fPriority.store(fPriority);
160+
m_fPriority.store(ValidatePriority(fPriority));
160161
}
161162

162163
virtual float DILIGENT_CALL_TYPE GetPriority() const override final
@@ -195,6 +196,16 @@ class AsyncTaskBase : public ObjectBase<IAsyncTask>
195196
std::atomic<bool> m_bSafelyCancel{false};
196197

197198
private:
199+
static float ValidatePriority(float fPriority) noexcept
200+
{
201+
if (std::isnan(fPriority))
202+
{
203+
LOG_ERROR_MESSAGE("Task priority must not be NaN. Using the default priority 0.");
204+
return 0;
205+
}
206+
return fPriority;
207+
}
208+
198209
mutable std::mutex m_StatusMtx;
199210
mutable std::condition_variable m_StatusChangedCond;
200211
std::atomic<float> m_fPriority{0};

Common/src/ThreadPool.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#include <algorithm>
3030
#include <mutex>
31+
#include <string>
3132
#include <thread>
3233
#include <map>
3334
#include <vector>
@@ -411,7 +412,7 @@ Uint64 PinWorkerThread(Uint32 ThreadId, Uint64 AllowedCoresMask)
411412
VERIFY_EXPR(AffinityMask != 0);
412413
Uint32 WorkerCore = PlatformMisc::GetLSB(AffinityMask);
413414
VERIFY_EXPR(WorkerCore < NumCores);
414-
Uint64 PrevMask = PlatformMisc::SetCurrentThreadAffinity(Uint64{1} << WorkerCore) != 0;
415+
Uint64 PrevMask = PlatformMisc::SetCurrentThreadAffinity(Uint64{1} << WorkerCore);
415416
if (PrevMask == 0)
416417
{
417418
LOG_WARNING_MESSAGE("Failed to pin worker thread ", ThreadId, " to core ", WorkerCore);

Tests/DiligentCoreTest/src/Common/ThreadPoolTest.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <chrono>
3333
#include <cmath>
3434
#include <future>
35+
#include <limits>
3536
#include <memory>
3637
#include <stdexcept>
3738
#include <thread>
@@ -426,6 +427,33 @@ TEST(Common_ThreadPool, WaitUntilRunningNotifiesAllWaitersWhenCancelled)
426427
"WaitUntilRunning did not wake all waiters when the task was cancelled");
427428
}
428429

430+
TEST(Common_ThreadPool, NaNTaskPriorityUsesDefault)
431+
{
432+
const float NaN = std::numeric_limits<float>::quiet_NaN();
433+
434+
// Expected behavior: NaN priority supplied at construction is rejected
435+
// because NaN cannot be used as a strict weak ordering key in the queue.
436+
{
437+
Testing::TestingEnvironment::ErrorScope ExpectedErrors{"Task priority must not be NaN"};
438+
RefCntAutoPtr<DummyTask> pTask;
439+
pTask = MakeNewRCObj<DummyTask>()(NaN);
440+
EXPECT_EQ(pTask->GetPriority(), 0.f);
441+
}
442+
443+
RefCntAutoPtr<DummyTask> pTask;
444+
pTask = MakeNewRCObj<DummyTask>()();
445+
pTask->SetPriority(10.f);
446+
EXPECT_EQ(pTask->GetPriority(), 10.f);
447+
448+
// Expected behavior: NaN priority supplied through SetPriority() is
449+
// rejected and the task falls back to the default priority.
450+
{
451+
Testing::TestingEnvironment::ErrorScope ExpectedErrors{"Task priority must not be NaN"};
452+
pTask->SetPriority(NaN);
453+
}
454+
EXPECT_EQ(pTask->GetPriority(), 0.f);
455+
}
456+
429457
TEST(Common_ThreadPool, RemoveTask)
430458
{
431459
constexpr Uint32 NumThreads = 4;

0 commit comments

Comments
 (0)