Skip to content

Commit efe9659

Browse files
committed
Destroy tasks as they are run in the thread pool
Without this, any RAII objects held in the task's captures aren't destroyed in a similar fashion to the task being run. If those objects in turn interact with the thread pool itself, chaos ensues. This comes up quite naturally with RAII-objects used for synchronization such as RAII-powered latches or releasing a mutex, etc. A unit test is crafted that tries to very directly test that the logic of the thread pool continues to hold even with an RAII object. This isn't the only type of failure mode (a deadlock due to mutexes in the captures can also occur), but seemed the easiest to test.
1 parent cdc6f85 commit efe9659

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

llvm/lib/Support/ThreadPool.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,13 @@ void StdThreadPool::processTasks(ThreadPoolTaskGroup *WaitingForGroup) {
110110
CurrentThreadTaskGroups->push_back(GroupOfTask);
111111
#endif
112112

113-
// Run the task we just grabbed
114-
Task();
113+
// Run the task we just grabbed. This also destroys the task once run to
114+
// release any resources held by it through RAII captured objects.
115+
//
116+
// It is particularly important to do this here so that we're not holding
117+
// any lock and any further operations on the thread or `ThreadPool` take
118+
// place here, at the same point as the task itself is executed.
119+
std::exchange(Task, {})();
115120

116121
#ifndef NDEBUG
117122
CurrentThreadTaskGroups->pop_back();

llvm/unittests/Support/ThreadPool.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "llvm/Support/ThreadPool.h"
1010
#include "llvm/ADT/STLExtras.h"
11+
#include "llvm/ADT/ScopeExit.h"
1112
#include "llvm/ADT/SetVector.h"
1213
#include "llvm/ADT/SmallVector.h"
1314
#include "llvm/Config/llvm-config.h" // for LLVM_ENABLE_THREADS
@@ -197,6 +198,42 @@ TYPED_TEST(ThreadPoolTest, AsyncMoveOnly) {
197198
ASSERT_EQ(42, f.get());
198199
}
199200

201+
TYPED_TEST(ThreadPoolTest, AsyncRAIICaptures) {
202+
CHECK_UNSUPPORTED();
203+
DefaultThreadPool Pool(hardware_concurrency(2));
204+
205+
// We use a task group and a non-atomic value to stress test that the chaining
206+
// of tasks via a captured RAII object in fact chains and synchronizes within
207+
// a group.
208+
ThreadPoolTaskGroup Group(Pool);
209+
int value = 0;
210+
211+
// Create an RAII object that when destroyed schedules more work. This makes
212+
// it easy to check that the RAII is resolved at the same point as a task runs
213+
// on the thread pool.
214+
auto schedule_next = llvm::make_scope_exit([&Group, &value] {
215+
// We sleep before scheduling the final task to make it much more likely
216+
// that an incorrect implementation actually exbitits a bug. Without the
217+
// sleep, we may get "lucky" and have the second task finish before the
218+
// assertion below fails even with an incorrect implementaiton. The
219+
// sleep is making _failures_ more reliable, it is not needed for
220+
// correctness and this test should only flakily _pass_, never flakily
221+
// fail.
222+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
223+
Group.async([&value] { value = 42; });
224+
});
225+
226+
// Now schedule the initial task, moving the RAII object to schedule the final
227+
// task into its captures.
228+
Group.async([schedule_next = std::move(schedule_next)]() {
229+
// Nothing to do here, the captured RAII object does the work.
230+
});
231+
232+
// Both tasks should complete here, synchronizing with the read of value.
233+
Group.wait();
234+
ASSERT_EQ(42, value);
235+
}
236+
200237
TYPED_TEST(ThreadPoolTest, GetFuture) {
201238
CHECK_UNSUPPORTED();
202239
DefaultThreadPool Pool(hardware_concurrency(2));

0 commit comments

Comments
 (0)