Skip to content

Commit 9f7b4cf

Browse files
RSNarafacebook-github-bot
authored andcommitted
Make RCTUnsafeExecuteOnMainQueueSync safer
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 88529c4 commit 9f7b4cf

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

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

Lines changed: 18 additions & 2 deletions
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/RuntimeExecutorSyncUIThreadUtils.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,12 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString *
311317
return;
312318
}
313319

314-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
320+
if (facebook::react::ReactNativeFeatureFlags::enableSaferMainQueueSyncDispatchOnIOS()) {
321+
if (RCTIsJSThread()) {
322+
facebook::react::postPotentiallyDeadlockingUITask(block).wait();
323+
return;
324+
}
325+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
315326
RCTLogError(
316327
@"RCTUnsafeExecuteOnMainQueueSync: %@",
317328
context ?: @"Sync dispatches to the main queue can deadlock React Native.");
@@ -340,7 +351,12 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp
340351
return;
341352
}
342353

343-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
354+
if (facebook::react::ReactNativeFeatureFlags::enableSaferMainQueueSyncDispatchOnIOS()) {
355+
if (RCTIsJSThread()) {
356+
facebook::react::postPotentiallyDeadlockingUITask(block).wait();
357+
return;
358+
}
359+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
344360
RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native.");
345361
}
346362

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <ReactCommon/RuntimeExecutor.h>
1111

1212
#include <jsi/jsi.h>
13+
#include <future>
1314

1415
namespace facebook::react {
1516

@@ -36,4 +37,8 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3637

3738
return data;
3839
}
40+
41+
std::future<void> postPotentiallyDeadlockingUITask(
42+
std::function<void()> runnable);
43+
3944
} // namespace facebook::react

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

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,110 @@
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/featureflags/ReactNativeFeatureFlags.h>
13+
#import <react/utils/OnScopeExit.h>
14+
#import <functional>
15+
#import <mutex>
16+
17+
namespace {
18+
struct UITask {
19+
std::promise<void> _isDone;
20+
std::mutex _mutex;
21+
std::function<void()> _uiWork;
22+
std::atomic_bool _hasStarted;
23+
24+
public:
25+
UITask(std::function<void()> uiWork) : _uiWork(uiWork), _hasStarted(false) {}
26+
27+
void run()
28+
{
29+
bool expected = false;
30+
if (!_hasStarted.compare_exchange_strong(expected, true)) {
31+
return;
32+
}
33+
facebook::react::OnScopeExit onScopeExit(^{
34+
_uiWork = nil;
35+
_isDone.set_value();
36+
});
37+
_uiWork();
38+
}
39+
40+
bool hasStarted()
41+
{
42+
return _hasStarted.load();
43+
}
44+
45+
std::future<void> getFuture()
46+
{
47+
return _isDone.get_future();
48+
}
49+
};
50+
} // namespace
51+
52+
static std::mutex _mutex;
53+
static std::condition_variable _cv;
54+
55+
// Global state
56+
static bool _isRunningPendingUITask = false;
57+
static std::shared_ptr<UITask> _pendingUITask;
1158

1259
namespace facebook::react {
60+
namespace {
61+
62+
void runPendingUITask()
63+
{
64+
OnScopeExit onScopeExit([&]() {
65+
_pendingUITask = nullptr;
66+
_isRunningPendingUITask = false;
67+
});
68+
_isRunningPendingUITask = true;
69+
_pendingUITask->run();
70+
}
71+
72+
void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
73+
const RuntimeExecutor &runtimeExecutor,
74+
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
75+
{
76+
react_native_assert([[NSThread currentThread] isMainThread] && !_isRunningPendingUITask);
77+
78+
jsi::Runtime *runtime = nullptr;
79+
std::mutex jsWorkDone;
80+
81+
jsWorkDone.lock();
82+
OnScopeExit onScopeExit([&]() { jsWorkDone.unlock(); });
83+
84+
{
85+
std::unique_lock<std::mutex> lock(_mutex);
86+
if (_pendingUITask) {
87+
runPendingUITask();
88+
}
89+
90+
runtimeExecutor([&](jsi::Runtime &rt) {
91+
runtime = &rt;
92+
_cv.notify_one();
93+
94+
// Block the js thread until jsWork finishes on calling thread
95+
jsWorkDone.lock();
96+
});
97+
98+
while (true) {
99+
_cv.wait(lock, [&] { return runtime != nullptr || _pendingUITask != nullptr; });
100+
101+
if (_pendingUITask != nullptr) {
102+
runPendingUITask();
103+
} else {
104+
break;
105+
}
106+
}
107+
}
108+
109+
jsWork(*runtime);
110+
}
111+
13112
/**
14113
* Example order of events (when not a sync call in runtimeExecutor
15114
* jsWork):
@@ -26,7 +125,7 @@
26125
* - [JS thread] Signal runtime capture block is finished:
27126
* runtimeCaptureBlockDone.unlock()
28127
*/
29-
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
128+
void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
30129
const RuntimeExecutor &runtimeExecutor,
31130
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
32131
{
@@ -41,7 +140,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
41140
jsWorkDone.lock();
42141
runtimeCaptureBlockDone.lock();
43142

44-
jsi::Runtime *runtimePtr;
143+
jsi::Runtime *runtimePtr = nullptr;
45144

46145
auto threadId = std::this_thread::get_id();
47146
auto runtimeCaptureBlock = [&](jsi::Runtime &runtime) {
@@ -67,4 +166,32 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
67166
runtimeCaptureBlockDone.lock();
68167
}
69168

169+
} // namespace
170+
171+
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
172+
const RuntimeExecutor &runtimeExecutor,
173+
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
174+
{
175+
if (ReactNativeFeatureFlags::enableSaferMainQueueSyncDispatchOnIOS()) {
176+
saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(jsWork));
177+
} else {
178+
legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(jsWork));
179+
}
180+
}
181+
182+
std::future<void> postPotentiallyDeadlockingUITask(std::function<void()> work)
183+
{
184+
std::lock_guard<std::mutex> lock(_mutex);
185+
react_native_assert((!_pendingUITask || _pendingUITask->hasStarted()));
186+
187+
auto uiTask = std::make_shared<UITask>(work);
188+
dispatch_async(dispatch_get_main_queue(), ^{
189+
uiTask->run();
190+
});
191+
192+
_pendingUITask = uiTask;
193+
_cv.notify_one();
194+
return uiTask->getFuture();
195+
}
196+
70197
} // namespace facebook::react

0 commit comments

Comments
 (0)