Skip to content

Commit 3d6bbef

Browse files
RSNarafacebook-github-bot
authored andcommitted
Introduce main queue coordinator (facebook#51425)
Summary: Pull Request resolved: facebook#51425 # Problem React native's new architecture will allow components to do sync render/events. That means they'll makes synchronous dispatches from main thread to the js thread, to capture the runtime so that they 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) Reviewed By: javache Differential Revision: D74769326
1 parent c64f698 commit 3d6bbef

File tree

5 files changed

+223
-7
lines changed

5 files changed

+223
-7
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
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

26+
using namespace facebook::react;
27+
2528
NSString *const RCTErrorUnspecified = @"EUNSPECIFIED";
2629

2730
// Returns the Path of Home directory
@@ -314,7 +317,12 @@ void RCTUnsafeExecuteOnMainQueueSyncWithError(dispatch_block_t block, NSString *
314317
return;
315318
}
316319

317-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
320+
if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
321+
unsafeExecuteOnMainThreadSync(block);
322+
return;
323+
}
324+
325+
if (ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
318326
RCTLogError(@"RCTUnsafeExecuteOnMainQueueSync: %@", context);
319327
}
320328

@@ -341,7 +349,12 @@ static void RCTUnsafeExecuteOnMainQueueOnceSync(dispatch_once_t *onceToken, disp
341349
return;
342350
}
343351

344-
if (facebook::react::ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
352+
if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
353+
unsafeExecuteOnMainThreadSync(block);
354+
return;
355+
}
356+
357+
if (ReactNativeFeatureFlags::disableMainQueueSyncDispatchIOS()) {
345358
RCTLogError(@"RCTUnsafeExecuteOnMainQueueOnceSync: Sync dispatches to the main queue can deadlock React Native.");
346359
}
347360

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-featureflags", version
48+
add_dependency(s, "React-debug")
49+
add_dependency(s, "React-utils", :additional_framework_paths => ["react/utils/platform/ios"])
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
@@ -24,7 +24,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2424
template <typename DataT>
2525
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
const RuntimeExecutor& runtimeExecutor,
27-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
27+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
2828
DataT data;
2929

3030
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
@@ -24,7 +24,7 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2424
template <typename DataT>
2525
inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
2626
const RuntimeExecutor& runtimeExecutor,
27-
std::function<DataT(jsi::Runtime& runtime)>&& runtimeWork) {
27+
std::function<DataT(jsi::Runtime&)>&& runtimeWork) {
2828
DataT data;
2929

3030
executeSynchronouslyOnSameThread_CAN_DEADLOCK(
@@ -33,4 +33,7 @@ inline static DataT executeSynchronouslyOnSameThread_CAN_DEADLOCK(
3333

3434
return data;
3535
}
36+
37+
void unsafeExecuteOnMainThreadSync(std::function<void()> work);
38+
3639
} // namespace facebook::react

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

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,158 @@
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+
28+
public:
29+
UITask(UITask &&other) = default;
30+
UITask &operator=(UITask &&other) = default;
31+
UITask(const UITask &) = delete;
32+
UITask &operator=(const UITask &) = delete;
33+
34+
UITask(std::function<void()> uiWork) : _uiWork(std::move(uiWork)) {}
35+
36+
void operator()()
37+
{
38+
if (!_uiWork) {
39+
return;
40+
}
41+
OnScopeExit onScopeExit(^{
42+
_uiWork = nullptr;
43+
_isDone.set_value();
44+
});
45+
_uiWork();
46+
}
47+
48+
std::future<void> future()
49+
{
50+
return _isDone.get_future();
51+
}
52+
};
53+
54+
// Protects access to g_uiTask and g_cv.
55+
std::mutex &g_mutex()
56+
{
57+
static std::mutex mutex;
58+
return mutex;
59+
}
60+
61+
std::condition_variable &g_cv()
62+
{
63+
static std::condition_variable cv;
64+
return cv;
65+
}
66+
67+
std::mutex &g_ticket()
68+
{
69+
static std::mutex ticket;
70+
return ticket;
71+
}
72+
73+
std::optional<UITask> &g_uiTask()
74+
{
75+
static std::optional<UITask> uiTaskQueue;
76+
return uiTaskQueue;
77+
}
78+
79+
// Must be called holding g_mutex();
80+
bool hasUITask()
81+
{
82+
return g_uiTask().has_value();
83+
}
84+
85+
// Must be called holding g_mutex();
86+
UITask takeUITask()
87+
{
88+
react_native_assert(hasUITask());
89+
auto uiTask = std::move(*g_uiTask());
90+
g_uiTask() = std::nullopt;
91+
return uiTask;
92+
}
93+
94+
// Must be called holding g_mutex();
95+
UITask &postUITask(std::function<void()> &&uiWork)
96+
{
97+
react_native_assert(!hasUITask());
98+
g_uiTask() = UITask(uiWork);
99+
g_cv().notify_one();
100+
return *g_uiTask();
101+
}
102+
103+
bool g_isRunningUITask = false;
104+
void runUITask(UITask &uiTask)
105+
{
106+
react_native_assert([[NSThread currentThread] isMainThread]);
107+
g_isRunningUITask = true;
108+
OnScopeExit onScopeExit([]() { g_isRunningUITask = false; });
109+
uiTask();
110+
}
111+
112+
/**
113+
* This method is resilient to multiple javascript threads.
114+
* This can happen when multiple react instances interleave.
115+
*
116+
* The extension from 1 js thread to n: All js threads race to
117+
* get a ticket to post a ui task. The first one to get the ticket
118+
* will post the ui task, and go to sleep. The cooridnator or
119+
* main queue will execute that ui task, waking up the js thread
120+
* and releasing that ticket. Another js thread will get the ticket.
121+
*
122+
* For simplicity, we will just use this algorithm for all bg threads.
123+
* Not just the js thread.
124+
*/
125+
void saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
126+
const RuntimeExecutor &runtimeExecutor,
127+
std::function<void(jsi::Runtime &runtime)> &&runtimeWork)
128+
{
129+
react_native_assert([[NSThread currentThread] isMainThread] && !g_isRunningUITask);
130+
131+
jsi::Runtime *runtime = nullptr;
132+
std::promise<void> runtimeWorkDone;
133+
134+
runtimeExecutor([&runtime, runtimeWorkDoneFuture = runtimeWorkDone.get_future().share()](jsi::Runtime &rt) {
135+
{
136+
std::lock_guard<std::mutex> lock(g_mutex());
137+
runtime = &rt;
138+
g_cv().notify_one();
139+
}
140+
141+
runtimeWorkDoneFuture.wait();
142+
});
143+
144+
while (true) {
145+
std::unique_lock<std::mutex> lock(g_mutex());
146+
g_cv().wait(lock, [&] { return runtime != nullptr || hasUITask(); });
147+
if (runtime != nullptr) {
148+
break;
149+
}
150+
151+
auto uiTask = takeUITask();
152+
lock.unlock();
153+
runUITask(uiTask);
154+
}
155+
156+
OnScopeExit onScopeExit([&]() { runtimeWorkDone.set_value(); });
157+
runtimeWork(*runtime);
158+
}
159+
13160
/*
14161
* Schedules `runtimeWork` to be executed on the same thread using the
15162
* `RuntimeExecutor`, and blocks on its completion.
@@ -26,7 +173,7 @@
26173
* - [JS thread] Signal runtime capture block is finished:
27174
* resolve(runtimeCaptureBlockDone);
28175
*/
29-
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
176+
void legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(
30177
const RuntimeExecutor &runtimeExecutor,
31178
std::function<void(jsi::Runtime &)> &&runtimeWork)
32179
{
@@ -58,4 +205,54 @@ void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
58205
runtimeCaptureBlockDone.get_future().wait();
59206
}
60207

208+
} // namespace
209+
210+
void executeSynchronouslyOnSameThread_CAN_DEADLOCK(
211+
const RuntimeExecutor &runtimeExecutor,
212+
std::function<void(jsi::Runtime &)> &&runtimeWork)
213+
{
214+
if (ReactNativeFeatureFlags::enableMainQueueCoordinatorOnIOS()) {
215+
saferExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
216+
} else {
217+
legacyExecuteSynchronouslyOnSameThread_CAN_DEADLOCK(runtimeExecutor, std::move(runtimeWork));
218+
}
219+
}
220+
221+
/**
222+
* This method is resilient to multiple javascript threads.
223+
* This can happen when multiple react instances interleave.
224+
*
225+
* The extension from 1 js thread to n: All js threads race to
226+
* get a ticket to post a ui task. The first one to get the ticket
227+
* will post the ui task, and go to sleep. The cooridnator or
228+
* main queue will execute that ui task, waking up the js thread
229+
* and releasing that ticket. Another js thread will get the ticket.
230+
*
231+
* For simplicity, we will just use this method for all bg threads.
232+
* Not just the js thread.
233+
*/
234+
void unsafeExecuteOnMainThreadSync(std::function<void()> work)
235+
{
236+
std::lock_guard<std::mutex> ticket(g_ticket());
237+
238+
std::future<void> isDone;
239+
{
240+
std::lock_guard<std::mutex> lock(g_mutex());
241+
isDone = postUITask(std::move(work)).future();
242+
}
243+
244+
dispatch_async(dispatch_get_main_queue(), ^{
245+
std::unique_lock<std::mutex> lock(g_mutex());
246+
if (!hasUITask()) {
247+
return;
248+
}
249+
250+
auto uiTask = takeUITask();
251+
lock.unlock();
252+
runUITask(uiTask);
253+
});
254+
255+
isDone.wait();
256+
}
257+
61258
} // namespace facebook::react

0 commit comments

Comments
 (0)