Skip to content

Commit c5e2f77

Browse files
javachemeta-codesync[bot]
authored andcommitted
Make runSync more robust (#55348)
Summary: Pull Request resolved: #55348 Changelog: [Internal] Reviewed By: Nurtau Differential Revision: D91779318 fbshipit-source-id: d196db8293889ecef9fbf48af6150da04f809325
1 parent 3782e93 commit c5e2f77

3 files changed

Lines changed: 54 additions & 16 deletions

File tree

packages/react-native/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ void MessageQueueThreadImpl::runOnQueue(std::function<void()>&& runnable) {
1515
if (!taskDispatchThread_.isRunning()) {
1616
return;
1717
}
18-
taskDispatchThread_.runAsync(
19-
[runnable = std::move(runnable)]() noexcept { runnable(); });
18+
taskDispatchThread_.runAsync(std::move(runnable));
2019
}
2120

2221
void MessageQueueThreadImpl::runOnQueueSync(std::function<void()>&& runnable) {
@@ -26,8 +25,7 @@ void MessageQueueThreadImpl::runOnQueueSync(std::function<void()>&& runnable) {
2625
if (taskDispatchThread_.isOnThread()) {
2726
runnable();
2827
} else {
29-
taskDispatchThread_.runSync(
30-
[runnable = std::move(runnable)]() noexcept { runnable(); });
28+
taskDispatchThread_.runSync(std::move(runnable));
3129
}
3230
}
3331

packages/react-native/ReactCxxPlatform/react/threading/TaskDispatchThread.cpp

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <folly/portability/SysResource.h>
1111
#include <folly/system/ThreadName.h>
12+
#include <cassert>
1213
#include <chrono>
1314
#include <future>
1415
#include <utility>
@@ -67,27 +68,30 @@ bool TaskDispatchThread::isRunning() noexcept {
6768
void TaskDispatchThread::runAsync(
6869
TaskFn&& task,
6970
std::chrono::milliseconds delayMs) noexcept {
71+
std::lock_guard<std::mutex> guard(mutex_);
7072
if (!running_) {
7173
return;
7274
}
73-
std::lock_guard<std::mutex> guard(mutex_);
7475
auto dispatchTime = std::chrono::system_clock::now() + delayMs;
7576
queue_.emplace(dispatchTime, std::move(task));
7677
loopCv_.notify_one();
7778
}
7879

7980
void TaskDispatchThread::runSync(TaskFn&& task) noexcept {
80-
if (!running_) {
81-
return;
82-
}
83-
std::promise<void> promise;
84-
runAsync([&]() {
85-
if (running_) {
86-
task();
87-
}
88-
promise.set_value();
89-
});
90-
promise.get_future().wait();
81+
// Calling runSync from the dispatch thread would deadlock, as the thread
82+
// would wait for itself to execute the task it just queued.
83+
assert(!isOnThread());
84+
85+
// Use shared_ptr to ensure the promise outlives both the caller and the
86+
// queued task. The destructor guard ensures the promise is fulfilled even
87+
// if the task is destroyed without executing (e.g., when quit() clears the
88+
// queue during shutdown).
89+
auto promise = std::make_shared<std::promise<void>>();
90+
auto guard = std::shared_ptr<void>(
91+
nullptr, [promise](void*) { promise->set_value(); });
92+
93+
runAsync([guard = std::move(guard), task = std::move(task)]() { task(); });
94+
promise->get_future().wait();
9195
}
9296

9397
void TaskDispatchThread::quit() noexcept {

packages/react-native/ReactCxxPlatform/react/threading/tests/TaskDispatchThreadTests.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,40 @@ TEST_F(TaskDispatchThreadTest, SyncTaskShouldntBeBlockedDueToDelayedTask) {
159159
dispatcher->runSync([&] { counter++; });
160160
EXPECT_EQ(counter.load(), 1);
161161
}
162+
163+
// Test: runSync should not block forever when quit() is called concurrently
164+
// This tests the fix where the RAII guard ensures the promise is fulfilled
165+
// even if the task is destroyed without executing during shutdown.
166+
TEST_F(TaskDispatchThreadTest, RunSyncDoesNotBlockWhenQuitCalledConcurrently) {
167+
for (int i = 0; i < 100; ++i) {
168+
auto localDispatcher = std::make_unique<TaskDispatchThread>();
169+
170+
std::atomic<bool> runSyncCompleted{false};
171+
172+
// Block the dispatcher thread with a slow task
173+
localDispatcher->runAsync(
174+
[&] { std::this_thread::sleep_for(std::chrono::milliseconds(10)); });
175+
176+
// Start runSync from another thread - it will queue a task and wait
177+
std::thread syncThread([&] {
178+
localDispatcher->runSync([&] {});
179+
runSyncCompleted = true;
180+
});
181+
182+
// Concurrently call quit() - this should cause the queued task to be
183+
// destroyed without executing, but runSync should still return (not block
184+
// forever) because the RAII guard fulfills the promise on destruction.
185+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
186+
localDispatcher->quit();
187+
188+
// If the fix works, syncThread should complete. If not, this would hang.
189+
syncThread.join();
190+
191+
// runSync should have completed (either task ran or was skipped due to
192+
// shutdown)
193+
EXPECT_TRUE(runSyncCompleted.load());
194+
195+
localDispatcher.reset();
196+
}
197+
}
162198
} // namespace facebook::react

0 commit comments

Comments
 (0)