Skip to content

Commit

Permalink
[browser][MT] Handling blocking wait (dotnet#99833)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelsavara authored and matouskozak committed Apr 30, 2024
1 parent d1fd532 commit 9edce38
Show file tree
Hide file tree
Showing 28 changed files with 298 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public partial class Thread
{
[ThreadStatic]
public static bool ThrowOnBlockingWaitOnJSInteropThread;
[ThreadStatic]
public static bool WarnOnBlockingWaitOnJSInteropThread;

public static void AssureBlockingPossible() { throw null; }
public static void ForceBlockingWait(Action<object?> action, object? state) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ T:System.Diagnostics.DebugProvider
M:System.Diagnostics.Debug.SetProvider(System.Diagnostics.DebugProvider)
M:System.Threading.Thread.AssureBlockingPossible
F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread
F:System.Threading.Thread.WarnOnBlockingWaitOnJSInteropThread
F:System.Threading.Thread.ForceBlockingWait
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public bool Wait(int timeoutMs, bool spinWait)
{
Debug.Assert(timeoutMs >= -1);

#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif

int spinCount = spinWait ? _spinCount : 0;

// Try to acquire the semaphore or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)

ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1);

#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif

if (!IsSet)
{
if (millisecondsTimeout == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,26 +729,46 @@ public static int GetCurrentProcessorId()
[ThreadStatic]
public static bool ThrowOnBlockingWaitOnJSInteropThread;

public static void AssureBlockingPossible()
[ThreadStatic]
public static bool WarnOnBlockingWaitOnJSInteropThread;

#pragma warning disable CS3001
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern unsafe void WarnAboutBlockingWait(char* stack, int length);

public static unsafe void AssureBlockingPossible()
{
if (ThrowOnBlockingWaitOnJSInteropThread)
{
throw new PlatformNotSupportedException(SR.WasmThreads_BlockingWaitNotSupportedOnJSInterop);
}
else if (WarnOnBlockingWaitOnJSInteropThread)
{
var st = $"Blocking the thread with JS interop is dangerous and could lead to deadlock. ManagedThreadId: {Environment.CurrentManagedThreadId}\n{Environment.StackTrace}";
fixed (char* stack = st)
{
WarnAboutBlockingWait(stack, st.Length);
}
}
}

#pragma warning restore CS3001

public static void ForceBlockingWait(Action<object?> action, object? state = null)
{
var flag = ThrowOnBlockingWaitOnJSInteropThread;
var wflag = WarnOnBlockingWaitOnJSInteropThread;
try
{
ThrowOnBlockingWaitOnJSInteropThread = false;
WarnOnBlockingWaitOnJSInteropThread = false;

action(state);
}
finally
{
ThrowOnBlockingWaitOnJSInteropThread = flag;
WarnOnBlockingWaitOnJSInteropThread = wflag;
}
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ internal bool WaitOneNoCheck(
SafeWaitHandle? waitHandle = _waitHandle;
ObjectDisposedException.ThrowIf(waitHandle is null, this);

#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif

bool success = false;
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,25 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
// arg_2 set by JS caller when there are arguments
// arg_3 set by JS caller when there are arguments
// arg_4 set by JS caller when there are arguments
#if !FEATURE_WASM_MANAGED_THREADS
try
{
#if FEATURE_WASM_MANAGED_THREADS
// when we arrive here, we are on the thread which owns the proxies
// if we need to dispatch the call to another thread in the future
// we may need to consider how to solve blocking of the synchronous call
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
arg_exc.AssertCurrentThreadContext();
#else
// when we arrive here, we are on the thread which owns the proxies
var ctx = arg_exc.AssertCurrentThreadContext();

if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
try
{
if (ctx.IsMainThread)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
}
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
{
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
}
}
#endif

Expand All @@ -156,9 +163,16 @@ public static void CallDelegate(JSMarshalerArgument* arguments_buffer)
#if FEATURE_WASM_MANAGED_THREADS
finally
{
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
if (ctx.IsMainThread)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
}
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
{
Thread.WarnOnBlockingWaitOnJSInteropThread = false;
}
}
}
#endif
Expand Down Expand Up @@ -189,12 +203,9 @@ public static void CompleteTask(JSMarshalerArgument* arguments_buffer)
}
}

if (holder.CallbackReady != null)
{
#pragma warning disable CA1416 // Validate platform compatibility
Thread.ForceBlockingWait(static (callbackReady) => ((ManualResetEventSlim)callbackReady!).Wait(), holder.CallbackReady);
#pragma warning restore CA1416 // Validate platform compatibility
}
// this is always running on I/O thread, so it will not throw PNSE
// it's also OK to block here, because we know we will only block shortly, as this is just race with the other thread.
holder.CallbackReady?.Wait();

lock (ctx)
{
Expand Down Expand Up @@ -247,21 +258,17 @@ public static void GetManagedStackTrace(JSMarshalerArgument* arguments_buffer)

// this is here temporarily, until JSWebWorker becomes public API
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker", "System.Runtime.InteropServices.JavaScript")]
// the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode)
// the marshaled signature is: GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode)
public static void InstallMainSynchronizationContext(JSMarshalerArgument* arguments_buffer)
{
ref JSMarshalerArgument arg_exc = ref arguments_buffer[0]; // initialized by caller in alloc_stack_frame()
ref JSMarshalerArgument arg_res = ref arguments_buffer[1];// initialized and set by caller
ref JSMarshalerArgument arg_1 = ref arguments_buffer[2];// initialized and set by caller
ref JSMarshalerArgument arg_2 = ref arguments_buffer[3];// initialized and set by caller
ref JSMarshalerArgument arg_3 = ref arguments_buffer[4];// initialized and set by caller
ref JSMarshalerArgument arg_4 = ref arguments_buffer[5];// initialized and set by caller

try
{
JSProxyContext.ThreadBlockingMode = (JSHostImplementation.JSThreadBlockingMode)arg_2.slot.Int32Value;
JSProxyContext.ThreadInteropMode = (JSHostImplementation.JSThreadInteropMode)arg_3.slot.Int32Value;
JSProxyContext.MainThreadingMode = (JSHostImplementation.MainThreadingMode)arg_4.slot.Int32Value;
var jsSynchronizationContext = JSSynchronizationContext.InstallWebWorkerInterop(true, CancellationToken.None);
jsSynchronizationContext.ProxyContext.JSNativeTID = arg_1.slot.IntPtrValue;
arg_res.slot.GCHandle = jsSynchronizationContext.ProxyContext.ContextHandle;
Expand All @@ -283,9 +290,16 @@ public static void BeforeSyncJSExport(JSMarshalerArgument* arguments_buffer)
{
var ctx = arg_exc.AssertCurrentThreadContext();
ctx.IsPendingSynchronousCall = true;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
if (ctx.IsMainThread)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
}
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
{
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
}
}
}
catch (Exception ex)
Expand All @@ -305,9 +319,16 @@ public static void AfterSyncJSExport(JSMarshalerArgument* arguments_buffer)
{
var ctx = arg_exc.AssertCurrentThreadContext();
ctx.IsPendingSynchronousCall = false;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.AllowBlockingWaitInAsyncCode)
if (ctx.IsMainThread)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;
}
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait)
{
Thread.WarnOnBlockingWaitOnJSInteropThread = false;
}
}
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span<JSMarshal
internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span<JSMarshalerArgument> arguments)
{
#if FEATURE_WASM_MANAGED_THREADS
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
}
else if (jsFunction.ProxyContext.IsPendingSynchronousCall)
if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
}
Expand All @@ -260,11 +256,7 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span<JS
internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span<JSMarshalerArgument> arguments)
{
#if FEATURE_WASM_MANAGED_THREADS
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
}
else if (jsFunction.ProxyContext.IsPendingSynchronousCall)
if (jsFunction.ProxyContext.IsPendingSynchronousCall && jsFunction.ProxyContext.IsMainThread)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
}
Expand All @@ -274,10 +266,8 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span<JSM

// we already know that we are not on the right thread
// this will be blocking until resolved by that thread
// we don't have to disable ThrowOnBlockingWaitOnJSInteropThread, because this is lock in native code
// we also don't throw PNSE here, because we know that the target has JS interop installed and that it could not block
// we know that the target has JS interop installed and that it could not block
// so it could take some time, while target is CPU busy, but not forever
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
Interop.Runtime.InvokeJSFunctionSend(jsFunction.ProxyContext.JSNativeTID, functionHandle, args);

ref JSMarshalerArgument exceptionArg = ref arguments[0];
Expand Down Expand Up @@ -317,11 +307,7 @@ internal static unsafe void InvokeJSImportImpl(JSFunctionBinding signature, Span
#if FEATURE_WASM_MANAGED_THREADS
else
{
if (JSProxyContext.ThreadInteropMode == JSHostImplementation.JSThreadInteropMode.NoSyncJSInterop)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS functions.");
}
else if (targetContext.IsPendingSynchronousCall)
if (targetContext.IsPendingSynchronousCall && targetContext.IsMainThread)
{
throw new PlatformNotSupportedException("Cannot call synchronous JS function from inside a synchronous call to a C# method.");
}
Expand Down Expand Up @@ -407,10 +393,6 @@ internal static unsafe void DispatchJSImportSyncSend(JSFunctionBinding signature

// we already know that we are not on the right thread
// this will be blocking until resolved by that thread
// we don't have to disable ThrowOnBlockingWaitOnJSInteropThread, because this is lock in native code
// we also don't throw PNSE here, because we know that the target has JS interop installed and that it could not block
// so it could take some time, while target is CPU busy, but not forever
// see also https://github.com/dotnet/runtime/issues/76958#issuecomment-1921418290
Interop.Runtime.InvokeJSImportSyncSend(targetContext.JSNativeTID, sig, args);

if (exc.slot.Type != MarshalerType.None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,41 +63,13 @@ public struct IntPtrAndHandle
internal RuntimeTypeHandle typeHandle;
}

// keep in sync with types\internal.ts
public enum MainThreadingMode : int
{
// Running the managed main thread on UI thread.
// Managed GC and similar scenarios could be blocking the UI.
// Easy to deadlock. Not recommended for production.
UIThread = 0,
// Running the managed main thread on dedicated WebWorker. Marshaling all JavaScript calls to and from the main thread.
DeputyThread = 1,
// TODO comments
DeputyAndIOThreads = 2,
}

// keep in sync with types\internal.ts
public enum JSThreadBlockingMode : int
{
// throw PlatformNotSupportedException if blocking .Wait is called on threads with JS interop, like JSWebWorker and Main thread.
// Avoids deadlocks (typically with pending JS promises on the same thread) by throwing exceptions.
NoBlockingWait = 0,
// TODO comments
AllowBlockingWaitInAsyncCode = 1,
// allow .Wait on all threads.
// Could cause deadlocks with blocking .Wait on a pending JS Task/Promise on the same thread or similar Task/Promise chain.
AllowBlockingWait = 100,
}

// keep in sync with types\internal.ts
public enum JSThreadInteropMode : int
{
// throw PlatformNotSupportedException if synchronous JSImport/JSExport is called on threads with JS interop, like JSWebWorker and Main thread.
// calling synchronous JSImport on thread pool or new threads is allowed.
NoSyncJSInterop = 0,
// allow non-re-entrant synchronous blocking calls to and from JS on JSWebWorker on threads with JS interop, like JSWebWorker and Main thread.
// calling synchronous JSImport on thread pool or new threads is allowed.
SimpleSynchronousJSInterop = 1,
PreventSynchronousJSExport = 0,
ThrowWhenBlockingWait = 1,
WarnWhenBlockingWait = 2,
DangerousAllowBlockingWait = 100,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,15 @@ private JSProxyContext()
public JSSynchronizationContext SynchronizationContext;
public JSAsyncTaskScheduler? AsyncTaskScheduler;

public static MainThreadingMode MainThreadingMode = MainThreadingMode.DeputyThread;
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode;
public static JSThreadInteropMode ThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop;
public static JSThreadBlockingMode ThreadBlockingMode = JSThreadBlockingMode.PreventSynchronousJSExport;
public bool IsPendingSynchronousCall;

#if !DEBUG
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public bool IsCurrentThread()
{
return ManagedTID == Environment.CurrentManagedThreadId && (!IsMainThread || MainThreadingMode == MainThreadingMode.UIThread);
return ManagedTID == Environment.CurrentManagedThreadId && !IsMainThread;
}

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "thread_id")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,18 @@ public static unsafe JSSynchronizationContext InstallWebWorkerInterop(bool isMai
ctx.previousSynchronizationContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(ctx);

if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.NoBlockingWait)
if (!isMainThread)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.ThrowWhenBlockingWait)
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = true;
}
else if (JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.WarnWhenBlockingWait
|| JSProxyContext.ThreadBlockingMode == JSHostImplementation.JSThreadBlockingMode.PreventSynchronousJSExport
)
{
Thread.WarnOnBlockingWaitOnJSInteropThread = true;
}
}

var proxyContext = ctx.ProxyContext;
Expand Down
Loading

0 comments on commit 9edce38

Please sign in to comment.