Skip to content

Commit 3b9e538

Browse files
RSNarafacebook-github-bot
authored andcommitted
RuntimeExecutor: Fix sync js execution from main thread
Summary: # Problem Sync rendering/events (i.e: execute js now below), when used in conjunction with js -> ui sync calls, can deadlock react native: ## Deadlock #1 **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime to be captured **JS thread**: execute ui code synchronously: * Js thread schedules a block on the ui thread * Js thread then goes to sleep, waiting for that block to execute. * **The application deadlocks** | {F1978009555} | {F1978009612} | ## Deadlock #2 **JS thread**: execute ui code synchronously: * Js thread schedules a block on the ui thread * Js thread then goes to sleep waiting for that block to execute. **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime to be captured * **The application deadlocks.** | {F1978009690} | {F1978009701} | # Changes This diff attempts to fix those deadlocks. How: * This diff introduces a stateful "execute js now" coordinator. * In "execute ui code synchronously" (js thread): * Before going to sleep, the js thread posts its ui work to the "execute js now" coordinator. * In "execute js now" (main thread): * Before trying to capture the runtime, the main thread executes "pending ui work", if it exists. * While the main thread is sleeping, waiting for runtime capture, it can be woken up, and asked to execute "pending ui work." ## Mitigation: Deadlock #1 **Main thread**: execute js now: * Main thread puts a block on the js queue, to capture the runtime. * Main thread then then goes to sleep, waiting for runtime to be captured **JS Thread**: execute ui code synchronously: * Js thread schedules this block on the ui thread. * ***New***: Js thread also assigns this block to the coordinator. *And wakes up the main thread*. * Js thread goes to sleep. The main thread wakes up: * Main thread **executes** the ui block assigned to the coordinator. **This cancels the ui block scheduled on the main queue.** * Main thread goes back to sleep. * The js thread wakes up, moves on to the next task. The runtime is captured by the main thread. | {F1978010383} | {F1978010363} | {F1978010371} | {F1978010379} ## Mitigation: Deadlock #2 **JS Thread**: execute ui code synchronously: * Js thread schedules this block on the ui thread. * ***New***: Js thread also assigns this block to the coordinator. *And wakes up the main thread*. * Js thread goes to sleep. **Main thread**: execute js now * Main thread executes the ui block immediately. (This cancels the ui block on the main queue). * Js thread wakes up and moves onto the next task. Main thread captures the runtime. | {F1978010525} | {F1978010533} | {F1978010542} | Differential Revision: D74769326
1 parent 54413cc commit 3b9e538

File tree

3 files changed

+131
-46
lines changed

3 files changed

+131
-46
lines changed

packages/react-native/React/Base/RCTUtils.mm

+26-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#import <CommonCrypto/CommonCrypto.h>
2020

2121
#import <React/RCTUtilsUIOverride.h>
22+
#import <ReactCommon/RuntimeExecutorUtils.h>
2223
#import "RCTAssert.h"
2324
#import "RCTLog.h"
2425

@@ -295,6 +296,11 @@ void RCTExecuteOnMainQueue(dispatch_block_t block)
295296
}
296297
}
297298

299+
static BOOL RCTIsJSThread()
300+
{
301+
return [[NSThread currentThread].name containsString:@"JavaScript"];
302+
}
303+
298304
// Please do not use this method
299305
// unless you know what you are doing.
300306
void RCTUnsafeExecuteOnMainQueueSync(dispatch_block_t block)
@@ -311,7 +317,16 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString *
311317
return;
312318
}
313319

314-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
320+
if (facebook::react::ReactNativeFeatureFlags::saferMainQueueSyncDispatch()) {
321+
if (RCTIsJSThread()) {
322+
auto uiTask = facebook::react::postPotentiallyDeadlockingUITask(block);
323+
dispatch_async(dispatch_get_main_queue(), ^{
324+
uiTask->run();
325+
});
326+
uiTask->waitUntilDone();
327+
return;
328+
}
329+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
315330
RCTLogError(
316331
@"RCTUnsafeExecuteOnMainQueueSync: %@",
317332
context ?: @"Sync dispatches to the main queue can deadlock React Native.");
@@ -340,7 +355,16 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp
340355
return;
341356
}
342357

343-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
358+
if (facebook::react::ReactNativeFeatureFlags::saferMainQueueSyncDispatch()) {
359+
if (RCTIsJSThread()) {
360+
auto uiTask = facebook::react::postPotentiallyDeadlockingUITask(block);
361+
dispatch_async(dispatch_get_main_queue(), ^{
362+
uiTask->run();
363+
});
364+
uiTask->waitUntilDone();
365+
return;
366+
}
367+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
344368
RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native.");
345369
}
346370

packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.h

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <ReactCommon/RuntimeExecutor.h>
99

1010
#include <jsi/jsi.h>
11+
#include <future>
1112

1213
namespace facebook::react {
1314

@@ -34,4 +35,8 @@ DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3435

3536
return data;
3637
}
38+
39+
std::future<void> schedulePotentiallyDeadlockingUITask(
40+
std::function<void()> runnable);
41+
3742
} // namespace facebook::react

packages/react-native/ReactCommon/runtimeexecutor/platform/ios/ReactCommon/RuntimeExecutorSyncUIThreadUtils.mm

+100-44
Original file line numberDiff line numberDiff line change
@@ -5,66 +5,122 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#import <Foundation/Foundation.h>
9+
810
#import <ReactCommon/RuntimeExecutorSyncUIThreadUtils.h>
9-
#include <mutex>
10-
#include <thread>
11+
#import <react/debug/react_native_assert.h>
12+
#import <react/utils/OnScopeExit.h>
13+
#import <functional>
14+
#import <mutex>
15+
16+
namespace {
17+
struct UITask {
18+
std::promise<void> _isDone;
19+
std::mutex _mutex;
20+
std::function<void()> _uiWork;
21+
std::atomic_bool _hasStarted;
22+
23+
public:
24+
UITask(std::function<void()> uiWork) : _uiWork(uiWork), _hasStarted(false) {}
25+
26+
void run()
27+
{
28+
bool expected = false;
29+
if (!_hasStarted.compare_exchange_strong(expected, true)) {
30+
return;
31+
}
32+
facebook::react::OnScopeExit onScopeExit(^{
33+
_uiWork = nil;
34+
_isDone.set_value();
35+
});
36+
_uiWork();
37+
}
38+
39+
bool hasStarted()
40+
{
41+
return _hasStarted.load();
42+
}
43+
44+
std::future<void> getFuture()
45+
{
46+
return _isDone.get_future();
47+
}
48+
};
49+
50+
static std::mutex _mutex;
51+
static std::condition_variable _cv;
52+
53+
// Global state
54+
static bool _isRunningPendingUITask = false;
55+
static std::shared_ptr<UITask> _pendingUITask;
56+
57+
void runPendingUITask()
58+
{
59+
facebook::react::OnScopeExit onScopeExit([&]() {
60+
_pendingUITask = nullptr;
61+
_isRunningPendingUITask = false;
62+
});
63+
_isRunningPendingUITask = true;
64+
_pendingUITask->run();
65+
}
66+
} // namespace
1167

1268
namespace facebook::react {
13-
/**
14-
* Example order of events (when not a sync call in runtimeExecutor
15-
* jsWork):
16-
* - [UI thread] Lock all mutexes at start
17-
* - [UI thread] Schedule "runtime capture block" on js thread
18-
* - [UI thread] Wait for runtime capture: runtimeCaptured.lock()
19-
* - [JS thread] Capture runtime by setting runtimePtr
20-
* - [JS thread] Signal runtime captured: runtimeCaptured.unlock()
21-
* - [UI thread] Call jsWork using runtimePtr
22-
* - [JS thread] Wait until jsWork done: jsWorkDone.lock()
23-
* - [UI thread] Signal jsWork done: jsWorkDone.unlock()
24-
* - [UI thread] Wait until runtime capture block finished:
25-
* runtimeCaptureBlockDone.lock()
26-
* - [JS thread] Signal runtime capture block is finished:
27-
* runtimeCaptureBlockDone.unlock()
28-
*/
2969
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3070
const RuntimeExecutor &runtimeExecutor,
3171
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
3272
{
33-
// Note: We need the third mutex to get back to the main thread before
34-
// the lambda is finished (because all mutexes are allocated on the stack).
73+
react_native_assert([[NSThread currentThread] isMainThread] && !_isRunningPendingUITask);
3574

36-
std::mutex runtimeCaptured;
75+
jsi::Runtime *runtime = nullptr;
3776
std::mutex jsWorkDone;
38-
std::mutex runtimeCaptureBlockDone;
39-
40-
runtimeCaptured.lock();
4177
jsWorkDone.lock();
42-
runtimeCaptureBlockDone.lock();
4378

44-
jsi::Runtime *runtimePtr;
79+
{
80+
std::unique_lock<std::mutex> lock(_mutex);
81+
if (_pendingUITask) {
82+
runPendingUITask();
83+
}
84+
85+
runtimeExecutor([&](jsi::Runtime &rt) {
86+
{
87+
std::lock_guard<std::mutex> lock(_mutex);
88+
runtime = &rt;
89+
_cv.notify_one();
90+
}
4591

46-
auto threadId = std::this_thread::get_id();
47-
auto runtimeCaptureBlock = [&](jsi::Runtime &runtime) {
48-
runtimePtr = &runtime;
92+
// Block the js thread until jsWork finishes on calling thread
93+
jsWorkDone.lock();
94+
});
4995

50-
if (threadId == std::this_thread::get_id()) {
51-
// In case of a synchronous call, we should unlock mutexes and return.
52-
runtimeCaptured.unlock();
53-
runtimeCaptureBlockDone.unlock();
54-
return;
55-
}
96+
while (true) {
97+
_cv.wait(lock, [&] { return runtime != nullptr || _pendingUITask != nullptr; });
5698

57-
runtimeCaptured.unlock();
58-
// `jsWork` is called somewhere here.
59-
jsWorkDone.lock();
60-
runtimeCaptureBlockDone.unlock();
61-
};
62-
runtimeExecutor(std::move(runtimeCaptureBlock));
99+
if (_pendingUITask != nullptr) {
100+
runPendingUITask();
101+
} else {
102+
break;
103+
}
104+
}
105+
}
63106

64-
runtimeCaptured.lock();
65-
jsWork(*runtimePtr);
107+
jsWork(*runtime);
66108
jsWorkDone.unlock();
67-
runtimeCaptureBlockDone.lock();
109+
}
110+
111+
std::future<void> schedulePotentiallyDeadlockingUITask(std::function<void()> work)
112+
{
113+
std::lock_guard<std::mutex> lock(_mutex);
114+
react_native_assert((!_pendingUITask || _pendingUITask->hasStarted()));
115+
116+
auto uiTask = std::make_shared<UITask>(work);
117+
dispatch_async(dispatch_get_main_queue(), ^{
118+
uiTask->run();
119+
});
120+
121+
_pendingUITask = uiTask;
122+
_cv.notify_one();
123+
return uiTask->getFuture();
68124
}
69125

70126
} // namespace facebook::react

0 commit comments

Comments
 (0)