Skip to content

Commit d99483e

Browse files
RSNarafacebook-github-bot
authored andcommitted
Make RCTUnsafeExecuteOnMainQueueSync safer
Summary: # Problem React native's new list component is doing synchronous render. That means it makes synchronous dispatches from main thread to the js thread. (To capture the runtime so that it can execute js on the main thread). But, the js thread already as a bunch of synchronous calls to the main thread. So, if any of those js -> ui sync calls happen concurrently with a synchronous render, the application will deadlock. This diff is an attempt to mitigate all those deadlocks. ## Context How js execution from the main thread works: * Main thread puts a block on the js thread, to capture the js runtime. * Main thread goes to sleep until that "runtime capture" block executes. * Js thread executes "runtime capture block". The runtime is captured for the main thread. The js thread is put to sleep, until the runtime is released. * Main thread wakes up, noticing that the runtime is captured. It executes its js code with the captured runtime. Then, it releases the runtime, and wakes up the js thread. Both the main and js thread move on to other tasks. How synchronous js -> main thread calls work: * Js thread puts a ui block on the main queue. * Js thread goes to sleep until that ui block executes on the main thread. ## 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. **Result:** The application deadlocks | {F1978009555} | {F1978009612} | ![image](https://github.com/user-attachments/assets/325a62f4-d5b7-492d-a114-efb738556239) ## 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 **Result:** The application deadlocks | {F1978009690} | {F1978009701} | ![image](https://github.com/user-attachments/assets/13a6ea17-a55d-453d-9291-d1c8007ecffa) # Changes This diff attempts to fix those deadlocks. How: * In "execute ui code synchronously" (js thread): * Before going to sleep, the js thread schedules the ui work on the main queue, **and** it posts the ui work to "execute js now". * In "execute js now" (main thread): * This diff makes "execute js now" stateful: it keeps a "pending ui block." * Before capturing the runtime, the "execute js now" executes "pending ui work", if it exists. * While sleeping waiting for runtime capture, "execute js now" can wake up, and execute "pending ui work." It goes back to sleep afterwards, waiting for runtime capture. ## 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 capture **JS Thread**: execute ui code synchronously: * Js thread puts its ui block on the ui queue. * ***New***: Js thread also posts that ui block to "execute js now". Main thread was sleeping waiting for runtime to be captured. It now wakes up. * Js thread goes to sleep. The main thread wakes up in "execute js now": * Main thread sees that a "pending ui block" is posted. It executes the "pending ui block." The block, also scheduled on the main thread, noops henceforth. * Main thread goes back to sleep, waiting for runtime capture. * The js thread wakes up, moves on to the next task. **Result:** The runtime is captured by the main thread. | {F1978010383} | {F1978010363} | {F1978010371} | {F1978010379} | ![image](https://github.com/user-attachments/assets/f53cb10c-7801-46be-934a-96af7d5f5fab) ## Mitigation: Deadlock #2 **JS Thread**: execute ui code synchronously: * Js thread puts its ui block on the ui queue. * ***New***: Js thread also posts that ui block to "execute js now". Main thread was sleeping waiting for runtime to be captured. It now wakes up. * Js thread goes to sleep. **Main thread**: execute js now * Main thread sees that a "pending ui block" is posted. It executes the "pending ui block" immediately. The block, also scheduled on the main thread, noops henceforth. * Js thread wakes up and moves onto the next task. **Result:** Main thread captures the runtime. | {F1978010525} | {F1978010533} | {F1978010542} | ![image](https://github.com/user-attachments/assets/9e0ca5ef-fab6-4a26-bcca-d79d36624d5d) Differential Revision: D74769326
1 parent aa839d7 commit d99483e

File tree

3 files changed

+162
-6
lines changed

3 files changed

+162
-6
lines changed

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

+18-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/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::unsafeExecuteOnMainThreadSync(block);
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::unsafeExecuteOnMainThreadSync(block);
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

+3
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3636

3737
return data;
3838
}
39+
40+
void unsafeExecuteOnMainThreadSync(std::function<void()> runnable);
41+
3942
} // namespace facebook::react

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

+141-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,119 @@
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 <future>
16+
#import <mutex>
17+
18+
namespace {
19+
struct UITask {
20+
std::promise<void> _isDone;
21+
std::mutex _mutex;
22+
std::function<void()> _uiWork;
23+
bool _isRunning;
24+
std::atomic_bool _wasStarted;
25+
26+
public:
27+
UITask(std::function<void()> uiWork) : _uiWork(uiWork), _isRunning(false), _wasStarted(false) {}
28+
29+
void run()
30+
{
31+
bool expected = false;
32+
if (!_wasStarted.compare_exchange_strong(expected, true)) {
33+
return;
34+
}
35+
facebook::react::OnScopeExit onScopeExit(^{
36+
_uiWork = nil;
37+
_isRunning = false;
38+
_isDone.set_value();
39+
});
40+
_isRunning = true;
41+
_uiWork();
42+
}
43+
44+
bool wasStarted()
45+
{
46+
return _wasStarted.load();
47+
}
48+
49+
bool isRunning()
50+
{
51+
return _isRunning;
52+
}
53+
54+
std::future<void> getFuture()
55+
{
56+
return _isDone.get_future();
57+
}
58+
};
59+
} // namespace
60+
61+
static std::mutex _mutex;
62+
static std::condition_variable _cv;
63+
64+
// Global state
65+
static std::shared_ptr<UITask> _postedUITask;
1166

1267
namespace facebook::react {
68+
namespace {
69+
70+
void runPostedUITask()
71+
{
72+
OnScopeExit onScopeExit([&]() { _postedUITask = nullptr; });
73+
_postedUITask->run();
74+
}
75+
76+
bool isJSThread()
77+
{
78+
return [[NSThread currentThread].name containsString:@"JavaScript"];
79+
}
80+
81+
void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
82+
const RuntimeExecutor &runtimeExecutor,
83+
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
84+
{
85+
react_native_assert([[NSThread currentThread] isMainThread] && (!_postedUITask || !_postedUITask->isRunning()));
86+
87+
jsi::Runtime *runtime = nullptr;
88+
std::mutex jsWorkDone;
89+
90+
jsWorkDone.lock();
91+
OnScopeExit onScopeExit([&]() { jsWorkDone.unlock(); });
92+
93+
{
94+
std::unique_lock<std::mutex> lock(_mutex);
95+
if (_postedUITask) {
96+
runPostedUITask();
97+
}
98+
99+
runtimeExecutor([&](jsi::Runtime &rt) {
100+
runtime = &rt;
101+
_cv.notify_one();
102+
103+
// Block the js thread until jsWork finishes on calling thread
104+
jsWorkDone.lock();
105+
});
106+
107+
while (true) {
108+
_cv.wait(lock, [&] { return runtime != nullptr || _postedUITask != nullptr; });
109+
110+
if (_postedUITask != nullptr) {
111+
runPostedUITask();
112+
} else {
113+
break;
114+
}
115+
}
116+
}
117+
118+
jsWork(*runtime);
119+
}
120+
13121
/**
14122
* Example order of events (when not a sync call in runtimeExecutor
15123
* jsWork):
@@ -26,7 +134,7 @@
26134
* - [JS thread] Signal runtime capture block is finished:
27135
* runtimeCaptureBlockDone.unlock()
28136
*/
29-
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
137+
void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
30138
const RuntimeExecutor &runtimeExecutor,
31139
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
32140
{
@@ -41,7 +149,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
41149
jsWorkDone.lock();
42150
runtimeCaptureBlockDone.lock();
43151

44-
jsi::Runtime *runtimePtr;
152+
jsi::Runtime *runtimePtr = nullptr;
45153

46154
auto threadId = std::this_thread::get_id();
47155
auto runtimeCaptureBlock = [&](jsi::Runtime &runtime) {
@@ -67,4 +175,33 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
67175
runtimeCaptureBlockDone.lock();
68176
}
69177

178+
} // namespace
179+
180+
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
181+
const RuntimeExecutor &runtimeExecutor,
182+
std::function<void(jsi::Runtime &runtime)> &&jsWork) noexcept
183+
{
184+
if (ReactNativeFeatureFlags::enableSaferMainQueueSyncDispatchOnIOS()) {
185+
saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(jsWork));
186+
} else {
187+
legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(jsWork));
188+
}
189+
}
190+
191+
void unsafeExecuteOnMainThreadSync(std::function<void()> work)
192+
{
193+
react_native_assert(isJSThread());
194+
std::lock_guard<std::mutex> lock(_mutex);
195+
react_native_assert(!_postedUITask || _postedUITask->wasStarted());
196+
197+
auto uiTask = std::make_shared<UITask>(work);
198+
dispatch_async(dispatch_get_main_queue(), ^{
199+
uiTask->run();
200+
});
201+
202+
_postedUITask = uiTask;
203+
_cv.notify_one();
204+
uiTask->getFuture().wait();
205+
}
206+
70207
} // namespace facebook::react

0 commit comments

Comments
 (0)