Skip to content

Commit a5cbe9a

Browse files
RSNarafacebook-github-bot
authored andcommitted
Introduce main queue coordinator (facebook#51425)
Summary: Pull Request resolved: facebook#51425 # 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 is put to sleep. * Js thread executes "runtime capture block". The runtime is captured for the main thread. The js thread is put to sleep. * Main thread wakes up, noticing that the runtime is captured. It executes its js code with the captured runtime. Then, it releases the runtime, which 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 facebook#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 facebook#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 facebook#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 facebook#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 c73167d commit a5cbe9a

File tree

5 files changed

+217
-7
lines changed

5 files changed

+217
-7
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

@@ -298,6 +299,11 @@ void RCTExecuteOnMainQueue(dispatch_block_t block)
298299
}
299300
}
300301

302+
static BOOL RCTIsJSThread()
303+
{
304+
return [[NSThread currentThread].name containsString:@"JavaScript"];
305+
}
306+
301307
// Please do not use this method
302308
// unless you know what you are doing.
303309
void RCTUnsafeExecuteOnMainQueueSync(dispatch_block_t block)
@@ -314,7 +320,12 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString *
314320
return;
315321
}
316322

317-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
323+
if (facebook::react::ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
324+
if (RCTIsJSThread()) {
325+
facebook::react::unsafeExecuteOnMainThreadSync(block);
326+
return;
327+
}
328+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
318329
RCTLogError(@"RCTUnsafeExecuteOnMainQueueSync: %@", context);
319330
}
320331

@@ -341,7 +352,12 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp
341352
return;
342353
}
343354

344-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
355+
if (facebook::react::ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
356+
if (RCTIsJSThread()) {
357+
facebook::react::unsafeExecuteOnMainThreadSync(block);
358+
return;
359+
}
360+
} else if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
345361
RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native.");
346362
}
347363

packages/react-native/ReactCommon/runtimeexecutor/React-runtimeexecutor.podspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,7 @@ Pod::Spec.new do |s|
4444
"DEFINES_MODULE" => "YES" }
4545

4646
s.dependency "React-jsi", version
47+
s.dependency "React-debug", version
48+
s.dependency "React-featureflags", version
49+
s.dependency "React-utils", version
4750
end

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
template <typename DataT>
2727
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2828
const RuntimeExecutor& runtimeExecutor,
29-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
29+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
3030
DataT data;
3131

3232
executeSynchronouslyOnSameThread_CAN_DEADLOCK(

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
template <typename DataT>
2727
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2828
const RuntimeExecutor& runtimeExecutor,
29-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
29+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
3030
DataT data;
3131

3232
executeSynchronouslyOnSameThread_CAN_DEADLOCK(
@@ -35,4 +35,7 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3535

3636
return data;
3737
}
38+
39+
void unsafeExecuteOnMainThreadSync(std::function<void()> runnable);
40+
3841
} // namespace facebook::react

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

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,141 @@
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 <future>
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 <algorithm>
15+
#import <functional>
16+
#import <future>
17+
#import <mutex>
18+
#import <optional>
19+
#import <thread>
1120

1221
namespace facebook::react {
22+
23+
namespace {
24+
class UITask {
25+
std::promise<void> _isDone;
26+
std::function<void()> _uiWork;
27+
long _id;
28+
static std::atomic_long _uid;
29+
30+
public:
31+
UITask(UITask &&other) = default;
32+
UITask &operator=(UITask &&other) = default;
33+
UITask(const UITask &) = delete;
34+
UITask &operator=(const UITask &) = delete;
35+
36+
UITask() = delete;
37+
UITask(std::function<void()> uiWork) : _isDone(), _uiWork(uiWork), _id(_uid.fetch_add(1)) {}
38+
39+
void run()
40+
{
41+
if (!_uiWork) {
42+
return;
43+
}
44+
OnScopeExit onScopeExit(^{
45+
_uiWork = nullptr;
46+
_isDone.set_value();
47+
});
48+
_uiWork();
49+
}
50+
51+
long id()
52+
{
53+
return _id;
54+
}
55+
56+
std::future<void> future()
57+
{
58+
return _isDone.get_future();
59+
}
60+
};
61+
62+
std::atomic_long UITask::_uid;
63+
64+
std::mutex &g_mutex()
65+
{
66+
static std::mutex mutex;
67+
return mutex;
68+
}
69+
70+
std::condition_variable &g_cv()
71+
{
72+
static std::condition_variable cv;
73+
return cv;
74+
}
75+
76+
std::mutex &g_ticket()
77+
{
78+
static std::mutex ticket;
79+
return ticket;
80+
}
81+
82+
std::optional<UITask> &g_uiTask()
83+
{
84+
static std::optional<UITask> uiTaskQueue;
85+
return uiTaskQueue;
86+
}
87+
88+
static bool g_isRunningUITask = false;
89+
90+
void runTask(UITask &task)
91+
{
92+
g_isRunningUITask = true;
93+
OnScopeExit onScopeExit([&]() { g_isRunningUITask = false; });
94+
task.run();
95+
}
96+
97+
/**
98+
* This method is resilient to multiple javascript threads.
99+
* This can happen when multiple react instances interleave.
100+
*
101+
* The extension from 1 js thread to n: All js threads race to
102+
* get a ticket to post a ui task. The first one to get the ticket
103+
* will post the ui task, and go to sleep. The cooridnator or
104+
* main queue will execute that ui task, waking up the js thread
105+
* and releasing that ticket. Another js thread will get the ticket.
106+
*/
107+
void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
108+
const RuntimeExecutor &runtimeExecutor,
109+
std::function<void(jsi::Runtime &runtime)> &&runtimeWork)
110+
{
111+
react_native_assert([[NSThread currentThread] isMainThread] && !g_isRunningUITask);
112+
113+
jsi::Runtime *runtime = nullptr;
114+
std::promise<void> runtimeWorkDone;
115+
116+
runtimeExecutor([&](jsi::Runtime &rt) {
117+
{
118+
std::lock_guard<std::mutex> lock(g_mutex());
119+
runtime = &rt;
120+
g_cv().notify_one();
121+
}
122+
123+
runtimeWorkDone.get_future().wait();
124+
});
125+
126+
while (true) {
127+
std::unique_lock<std::mutex> lock(g_mutex());
128+
g_cv().wait(lock, [&] { return runtime != nullptr || g_uiTask() != std::nullopt; });
129+
if (runtime != nullptr) {
130+
break;
131+
}
132+
133+
auto uiTask = std::move(*g_uiTask());
134+
g_uiTask() = std::nullopt;
135+
lock.unlock();
136+
runTask(uiTask);
137+
}
138+
139+
OnScopeExit onScopeExit([&]() { runtimeWorkDone.set_value(); });
140+
runtimeWork(*runtime);
141+
}
142+
13143
/*
14144
* Executes some `runtimeWork` on the same thread in a *synchronous* manner
15145
* using the `RuntimeExecutor`.
@@ -26,7 +156,7 @@
26156
* - [JS thread] Signal runtime capture block is finished:
27157
* resolve(runtimeCaptureBlockDone);
28158
*/
29-
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
159+
void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
30160
const RuntimeExecutor &runtimeExecutor,
31161
std::function<void(jsi::Runtime &)> &&runtimeWork)
32162
{
@@ -55,4 +185,62 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
55185
runtimeCaptureBlockDone.get_future().wait();
56186
}
57187

188+
} // namespace
189+
190+
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
191+
const RuntimeExecutor &runtimeExecutor,
192+
std::function<void(jsi::Runtime &)> &&runtimeWork)
193+
{
194+
if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
195+
saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
196+
} else {
197+
legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
198+
}
199+
}
200+
201+
/**
202+
* This method is resilient to multiple javascript threads.
203+
* This can happen when multiple react instances interleave.
204+
*
205+
* The extension from 1 js thread to n: All js threads race to
206+
* get a ticket to post a ui task. The first one to get the ticket
207+
* will post the ui task, and go to sleep. The cooridnator or
208+
* main queue will execute that ui task, waking up the js thread
209+
* and releasing that ticket. Another js thread will get the ticket.
210+
*/
211+
void unsafeExecuteOnMainThreadSync(std::function<void()> work)
212+
{
213+
std::lock_guard<std::mutex> ticket(g_ticket());
214+
react_native_assert([[NSThread currentThread].name containsString:@"JavaScript"]);
215+
{
216+
std::lock_guard lock(g_mutex());
217+
react_native_assert(!g_uiTask());
218+
}
219+
220+
long tid = -1;
221+
std::future<void> isDone;
222+
223+
// post ui task to main thread
224+
{
225+
std::lock_guard<std::mutex> lock(g_mutex());
226+
g_uiTask() = UITask(work);
227+
tid = g_uiTask()->id();
228+
isDone = g_uiTask()->future();
229+
g_cv().notify_one();
230+
}
231+
232+
dispatch_async(dispatch_get_main_queue(), ^{
233+
std::unique_lock<std::mutex> lock;
234+
if (!g_uiTask() || g_uiTask()->id() != tid) {
235+
return;
236+
}
237+
auto uiTask = std::move(*g_uiTask());
238+
g_uiTask() = std::nullopt;
239+
lock.unlock();
240+
runTask(uiTask);
241+
});
242+
243+
isDone.wait();
244+
}
245+
58246
} // namespace facebook::react

0 commit comments

Comments
 (0)