diff --git a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs index b16548c7b4c37..45a5b9f037269 100644 --- a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs +++ b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.cs @@ -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 action, object? state) { throw null; } diff --git a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt index 3b80cb0de6753..2a6434973ff1e 100644 --- a/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt +++ b/src/libraries/System.Private.CoreLib/ref/System.Private.CoreLib.ExtraApis.txt @@ -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 diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs index 7f7bddf24737b..39233c87c15c9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLifoSemaphore.cs @@ -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 diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs index a385543f9174a..516fb42bf0a52 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @@ -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) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs index 9d3fd7a0466d7..3ef77076a0198 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs @@ -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 action, object? state = null) { var flag = ThrowOnBlockingWaitOnJSInteropThread; + var wflag = WarnOnBlockingWaitOnJSInteropThread; try { ThrowOnBlockingWaitOnJSInteropThread = false; + WarnOnBlockingWaitOnJSInteropThread = false; action(state); } finally { ThrowOnBlockingWaitOnJSInteropThread = flag; + WarnOnBlockingWaitOnJSInteropThread = wflag; } } #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs index 21920bc39b754..d215a82cd3234 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs @@ -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 { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs index 6faf786f3bd53..c7bb4a81d3bd5 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/JavaScriptExports.cs @@ -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 @@ -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 @@ -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) { @@ -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; @@ -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) @@ -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) diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs index 61ea2a8546796..f2d908d947074 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSFunctionBinding.cs @@ -230,11 +230,7 @@ internal static unsafe void InvokeJSFunction(JSObject jsFunction, Span 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."); } @@ -260,11 +256,7 @@ internal static unsafe void InvokeJSFunctionCurrent(JSObject jsFunction, Span 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."); } @@ -274,10 +266,8 @@ internal static unsafe void DispatchJSFunctionSync(JSObject jsFunction, Span(async () => { CancellationTokenSource cts = new CancellationTokenSource(); var promise = response.Content.ReadAsStringAsync(cts.Token); - Console.WriteLine("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("HttpClient_CancelInDifferentThread: ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); cts.Cancel(); var res = await promise; throw new Exception("This should be unreachable: " + res); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs index c67bac997294b..0a2ae44142dc3 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs @@ -11,15 +11,9 @@ namespace System.Runtime.InteropServices.JavaScript.Tests // TODO test: // JSExport 2x - // JSExport async - // lock - // thread allocation, many threads // ProxyContext flow, child thread, child task // use JSObject after JSWebWorker finished, especially HTTP - // WS on JSWebWorker - // HTTP continue on TP // event pipe - // FS // JS setTimeout till after JSWebWorker close // synchronous .Wait for JS setTimeout on the same thread -> deadlock problem **7)** @@ -159,7 +153,7 @@ public async Task JSSynchronizationContext_Send_Post_Items_Cancellation() } catch (Exception ex) { - Console.WriteLine("Unexpected exception " + ex); + WebWorkerTestHelper.Log("Unexpected exception " + ex); postReady.SetException(ex); return Task.FromException(ex); } @@ -344,7 +338,7 @@ public async Task ManagedConsole(Executor executor) using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(() => { - Console.WriteLine("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); + WebWorkerTestHelper.Log("C# Hello from ManagedThreadId: " + Environment.CurrentManagedThreadId); Console.Clear(); return Task.CompletedTask; }, cts.Token); @@ -392,7 +386,7 @@ public async Task ThreadingTimer(Executor executor) await executor.Execute(async () => { TaskCompletionSource tcs = new TaskCompletionSource(); - Console.WriteLine("ThreadingTimer: Start Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("ThreadingTimer: Start Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); using var timer = new Timer(_ => { @@ -405,7 +399,7 @@ await executor.Execute(async () => await tcs.Task; }, cts.Token); - Console.WriteLine("ThreadingTimer: End Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); + WebWorkerTestHelper.Log("ThreadingTimer: End Time: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); Assert.True(hit); } @@ -496,7 +490,7 @@ await executor.Execute(async () => } [Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))] - public async Task WaitDoesNotAssertInAsyncCode(Executor executor, NamedCall method) + public async Task WaitInAsyncAssertsOnlyOnJSWebWorker(Executor executor, NamedCall method) { using var cts = CreateTestCaseTimeoutSource(); await executor.Execute(async () => @@ -513,7 +507,15 @@ await executor.Execute(async () => exception = ex; } - Assert.Null(exception); + if (method.IsBlocking && executor.Type == ExecutorType.JSWebWorker) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } }, cts.Token); } @@ -527,7 +529,8 @@ await executor.Execute(async () => Exception? exception = null; // the callback will hit Main or JSWebWorker, not the original executor thread - await WebWorkerTestHelper.CallMeBackSync(() => { + await WebWorkerTestHelper.CallMeBackSync(() => + { // when we are inside of synchronous callback, all blocking .Wait is forbidden try { @@ -539,9 +542,15 @@ await WebWorkerTestHelper.CallMeBackSync(() => { } }); - Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); - Assert.NotNull(exception); - Assert.IsType(exception); + if (method.IsBlocking) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } }, cts.Token); } @@ -558,9 +567,15 @@ await executor.Execute(async () => // the callback will hit Main or JSWebWorker, not the original executor thread await WebWorkerTestHelper.CallExportBackSync(nameof(WebWorkerTestHelper.CallCurrentCallback)); - Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId); - Assert.NotNull(WebWorkerTestHelper.LastException); - Assert.IsType(WebWorkerTestHelper.LastException); + if (method.IsBlocking) + { + Assert.NotNull(WebWorkerTestHelper.LastException); + Assert.IsType(WebWorkerTestHelper.LastException); + } + else + { + Assert.Null(WebWorkerTestHelper.LastException); + } }, cts.Token); } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs index 1df4c61e6bcbc..77aef0857a8d2 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; using System.Threading.Tasks; using System.Threading; using Xunit; @@ -35,7 +36,7 @@ protected CancellationTokenSource CreateTestCaseTimeoutSource([CallerMemberName] cts.Token.Register(() => { var end = DateTime.Now; - Console.WriteLine($"Unexpected test case {memberName} timeout after {end - start} ManagedThreadId:{Environment.CurrentManagedThreadId}"); + WebWorkerTestHelper.Log($"Unexpected test case {memberName} timeout after {end - start} ManagedThreadId:{Environment.CurrentManagedThreadId}"); }); return cts; } @@ -90,7 +91,7 @@ async Task ActionsInDifferentThreads1() } catch (Exception ex) { - Console.WriteLine("ActionsInDifferentThreads1 failed\n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads1 failed\n" + ex); job1ReadyTCS.SetResult(default); e1Failed = true; throw; @@ -137,36 +138,74 @@ async Task ActionsInDifferentThreads2() } if (!e1Done || !e2Done) { - Console.WriteLine("ActionsInDifferentThreads canceling because of unexpected fail: \n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads canceling because of unexpected fail: \n" + ex); cts.Cancel(); } else { - Console.WriteLine("ActionsInDifferentThreads failed with: \n" + ex); + WebWorkerTestHelper.Log("ActionsInDifferentThreads failed with: \n" + ex); } throw; } } + static void LocalCtsIgnoringCall(Action action) + { + var cts = new CancellationTokenSource(8); + try + { + action(cts.Token); + } + catch (OperationCanceledException exception) + { + if (exception.CancellationToken != cts.Token) + { + throw; + } + /* ignore the local one */ + } + } + public static IEnumerable BlockingCalls = new List { - new NamedCall { Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(10, ct).Wait(ct); }}, - new NamedCall { Name = "Task.WaitAll", Call = delegate (CancellationToken ct) { Task.WaitAll(Task.Delay(10, ct)); }}, - new NamedCall { Name = "Task.WaitAny", Call = delegate (CancellationToken ct) { Task.WaitAny(Task.Delay(10, ct)); }}, - new NamedCall { Name = "ManualResetEventSlim.Wait", Call = delegate (CancellationToken ct) { - using var mr = new ManualResetEventSlim(false); - using var cts = new CancellationTokenSource(8); - try { - mr.Wait(cts.Token); - } catch (OperationCanceledException) { /* ignore */ } - }}, - new NamedCall { Name = "SemaphoreSlim.Wait", Call = delegate (CancellationToken ct) { - using var sem = new SemaphoreSlim(2); - var cts = new CancellationTokenSource(8); - try { - sem.Wait(cts.Token); - } catch (OperationCanceledException) { /* ignore */ } - }}, + // things that should NOT throw PNSE + new NamedCall { IsBlocking = false, Name = "Console.WriteLine", Call = delegate (CancellationToken ct) { Console.WriteLine("Blocking"); }}, + new NamedCall { IsBlocking = false, Name = "Directory.GetCurrentDirectory", Call = delegate (CancellationToken ct) { Directory.GetCurrentDirectory(); }}, + new NamedCall { IsBlocking = false, Name = "CancellationTokenSource.ctor", Call = delegate (CancellationToken ct) { + using var cts = new CancellationTokenSource(8); + }}, + new NamedCall { IsBlocking = false, Name = "Task.Delay", Call = delegate (CancellationToken ct) { + Task.Delay(30, ct); + }}, + new NamedCall { IsBlocking = false, Name = "new Timer", Call = delegate (CancellationToken ct) { + new Timer((_) => { }, null, 1, -1); + }}, + + // things which should throw PNSE on sync JSExport and JSWebWorker + new NamedCall { IsBlocking = true, Name = "Task.Wait", Call = delegate (CancellationToken ct) { Task.Delay(30, ct).Wait(ct); }}, + new NamedCall { IsBlocking = true, Name = "Task.WaitAll", Call = delegate (CancellationToken ct) { Task.WaitAll(Task.Delay(30, ct)); }}, + new NamedCall { IsBlocking = true, Name = "Task.WaitAny", Call = delegate (CancellationToken ct) { Task.WaitAny(Task.Delay(30, ct)); }}, + new NamedCall { IsBlocking = true, Name = "ManualResetEventSlim.Wait", Call = delegate (CancellationToken ct) { + using var mr = new ManualResetEventSlim(false); + LocalCtsIgnoringCall(mr.Wait); + }}, + new NamedCall { IsBlocking = true, Name = "SemaphoreSlim.Wait", Call = delegate (CancellationToken ct) { + using var sem = new SemaphoreSlim(2); + LocalCtsIgnoringCall(sem.Wait); + }}, + new NamedCall { IsBlocking = true, Name = "Mutex.WaitOne", Call = delegate (CancellationToken ct) { + using var mr = new ManualResetEventSlim(false); + var mutex = new Mutex(); + var thread = new Thread(() => { + mutex.WaitOne(); + mr.Set(); + Thread.Sleep(50); + mutex.ReleaseMutex(); + }); + thread.Start(); + Thread.ForceBlockingWait(static (b) => ((ManualResetEventSlim)b).Wait(), mr); + mutex.WaitOne(); + }}, }; public static IEnumerable GetTargetThreadsAndBlockingCalls() diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs index 9a1856780d5d5..fa83846f14929 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTestHelper.cs @@ -385,10 +385,10 @@ public static Task RunOnTargetAsync(SynchronizationContext ctx, Func job, public class NamedCall { public string Name { get; set; } + public bool IsBlocking { get; set; } public delegate void Method(CancellationToken ct); public Method Call { get; set; } override public string ToString() => Name; } - } diff --git a/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml b/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml index 5fc41e30ed441..8eb2f78e6a706 100644 --- a/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml +++ b/src/libraries/System.Threading.Thread/src/CompatibilitySuppressions.Threading.xml @@ -20,6 +20,10 @@ CP0002 F:System.Threading.Thread.ThrowOnBlockingWaitOnJSInteropThread + + CP0002 + F:System.Threading.Thread.WarnOnBlockingWaitOnJSInteropThread + CP0002 M:System.Threading.Thread.AssureBlockingPossible diff --git a/src/mono/browser/runtime/corebindings.c b/src/mono/browser/runtime/corebindings.c index 37a8598ec6c4e..485ba3189af6e 100644 --- a/src/mono/browser/runtime/corebindings.c +++ b/src/mono/browser/runtime/corebindings.c @@ -51,6 +51,7 @@ void mono_wasm_invoke_js_function_send (pthread_t target_tid, int function_js_ha extern void mono_threads_wasm_async_run_in_target_thread_vi (pthread_t target_thread, void (*func) (gpointer), gpointer user_data1); extern void mono_threads_wasm_async_run_in_target_thread_vii (pthread_t target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer user_data2); extern void mono_threads_wasm_sync_run_in_target_thread_vii (pthread_t target_thread, void (*func) (gpointer, gpointer), gpointer user_data1, gpointer args); +extern void mono_wasm_warn_about_blocking_wait (void* ptr, int32_t length); #else extern void* mono_wasm_bind_js_import_ST (void *signature); extern void mono_wasm_invoke_jsimport_ST (int function_handle, void *args); @@ -86,6 +87,7 @@ void bindings_initialize_internals (void) mono_add_internal_call ("Interop/Runtime::InvokeJSImportAsyncPost", mono_wasm_invoke_jsimport_async_post); mono_add_internal_call ("Interop/Runtime::InvokeJSFunctionSend", mono_wasm_invoke_js_function_send); mono_add_internal_call ("Interop/Runtime::CancelPromisePost", mono_wasm_cancel_promise_post); + mono_add_internal_call ("System.Threading.Thread::WarnAboutBlockingWait", mono_wasm_warn_about_blocking_wait); #else mono_add_internal_call ("Interop/Runtime::BindJSImportST", mono_wasm_bind_js_import_ST); mono_add_internal_call ("Interop/Runtime::InvokeJSImportST", mono_wasm_invoke_jsimport_ST); diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index bcecfec8b10ea..f5420a3a729e1 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -29,12 +29,11 @@ import { mono_wasm_cancel_promise } from "./cancelable-promise"; import { mono_wasm_start_deputy_thread_async, mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered, - mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_start_io_thread_async + mono_wasm_pthread_on_pthread_registered, mono_wasm_pthread_set_name, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_start_io_thread_async, mono_wasm_warn_about_blocking_wait } from "./pthreads"; import { mono_wasm_dump_threads } from "./pthreads/ui-thread"; import { mono_wasm_schedule_synchronization_context } from "./pthreads/shared"; - // the JS methods would be visible to EMCC linker and become imports of the WASM module export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ @@ -58,6 +57,7 @@ export const mono_wasm_threads_imports = !WasmEnableThreads ? [] : [ mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_invoke_jsimport_MT, + mono_wasm_warn_about_blocking_wait, ]; export const mono_wasm_imports = [ diff --git a/src/mono/browser/runtime/interp-pgo.ts b/src/mono/browser/runtime/interp-pgo.ts index 0a5a6e6c72853..14697385800b1 100644 --- a/src/mono/browser/runtime/interp-pgo.ts +++ b/src/mono/browser/runtime/interp-pgo.ts @@ -206,9 +206,7 @@ export async function getCacheKey (prefix: string): Promise { delete inputs.enableDownloadRetry; delete inputs.extensions; delete inputs.runtimeId; - delete inputs.mainThreadingMode; delete inputs.jsThreadBlockingMode; - delete inputs.jsThreadInteropMode; inputs.GitHash = loaderHelpers.gitHash; inputs.ProductVersion = ProductVersion; diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index 8fd7f00fe3152..5ff185827c106 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -4,7 +4,7 @@ import BuildConfiguration from "consts:configuration"; import WasmEnableThreads from "consts:wasmEnableThreads"; -import { MainThreadingMode, type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode, JSThreadInteropMode } from "../types/internal"; +import { type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode } from "../types/internal"; import type { DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types"; import { exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_error, mono_log_debug } from "./logging"; @@ -12,7 +12,6 @@ import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryI import { mono_exit } from "./exit"; import { makeURLAbsoluteWithApplicationBase } from "./polyfills"; import { appendUniqueQuery } from "./assets"; -import { mono_log_warn } from "./logging"; export function deep_merge_config (target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { // no need to merge the same object @@ -198,38 +197,8 @@ export function normalizeConfig () { if (!Number.isInteger(config.finalizerThreadStartDelayMs)) { config.finalizerThreadStartDelayMs = 200; } - if (config.mainThreadingMode == undefined) { - config.mainThreadingMode = MainThreadingMode.DeputyAndIOThreads; - } if (config.jsThreadBlockingMode == undefined) { - config.jsThreadBlockingMode = JSThreadBlockingMode.AllowBlockingWaitInAsyncCode; - } - if (config.jsThreadInteropMode == undefined) { - config.jsThreadInteropMode = JSThreadInteropMode.SimpleSynchronousJSInterop; - } - let validModes = false; - if (config.mainThreadingMode == MainThreadingMode.DeputyThread - && config.jsThreadBlockingMode == JSThreadBlockingMode.NoBlockingWait - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } else if (config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads - && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWaitInAsyncCode - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } else if (config.mainThreadingMode == MainThreadingMode.DeputyThread - && config.jsThreadBlockingMode == JSThreadBlockingMode.AllowBlockingWait - && config.jsThreadInteropMode == JSThreadInteropMode.SimpleSynchronousJSInterop - ) { - validModes = true; - } - if (!validModes) { - mono_log_warn("Unsupported threading configuration", { - mainThreadingMode: config.mainThreadingMode, - jsThreadBlockingMode: config.jsThreadBlockingMode, - jsThreadInteropMode: config.jsThreadInteropMode - }); + config.jsThreadBlockingMode = JSThreadBlockingMode.PreventSynchronousJSExport; } } diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts index f90fbb049d150..065136faaba47 100644 --- a/src/mono/browser/runtime/managed-exports.ts +++ b/src/mono/browser/runtime/managed-exports.ts @@ -3,7 +3,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; -import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadInteropMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod, PThreadPtr } from "./types/internal"; +import { GCHandle, GCHandleNull, JSMarshalerArguments, JSThreadBlockingMode, MarshalerToCs, MarshalerToJs, MarshalerType, MonoMethod, PThreadPtr } from "./types/internal"; import cwraps, { threads_c_functions as twraps } from "./cwraps"; import { runtimeHelpers, Module, loaderHelpers, mono_assert } from "./globals"; import { JavaScriptMarshalerArgSize, alloc_stack_frame, get_arg, get_arg_gc_handle, is_args_exception, set_arg_i32, set_arg_intptr, set_arg_type, set_gc_handle, set_receiver_should_free } from "./marshal"; @@ -165,10 +165,12 @@ export function complete_task (holder_gc_handle: GCHandle, error?: any, data?: a export function call_delegate (callback_gc_handle: GCHandle, arg1_js: any, arg2_js: any, arg3_js: any, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs) { loaderHelpers.assert_runtime_running(); if (WasmEnableThreads) { - if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { - throw new Error("Cannot call synchronous C# methods."); - } else if (runtimeHelpers.isPendingSynchronousCall) { - throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + if (monoThreadInfo.isUI) { + if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) { + throw new Error("Cannot call synchronous C# methods."); + } else if (runtimeHelpers.isPendingSynchronousCall) { + throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + } } } const sp = Module.stackSave(); @@ -225,26 +227,39 @@ export function get_managed_stack_trace (exception_gc_handle: GCHandle) { } } -// GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode, JSThreadInteropMode jsThreadInteropMode, MainThreadingMode mainThreadingMode) -export function install_main_synchronization_context (jsThreadBlockingMode: number, jsThreadInteropMode: number, mainThreadingMode: number): GCHandle { +// GCHandle InstallMainSynchronizationContext(nint jsNativeTID, JSThreadBlockingMode jsThreadBlockingMode) +export function install_main_synchronization_context (jsThreadBlockingMode: JSThreadBlockingMode): GCHandle { if (!WasmEnableThreads) return GCHandleNull; assert_c_interop(); try { // this block is like alloc_stack_frame() but without set_args_context() - const bytes = JavaScriptMarshalerArgSize * 6; + const bytes = JavaScriptMarshalerArgSize * 4; const args = Module.stackAlloc(bytes) as any; _zero_region(args, bytes); const res = get_arg(args, 1); const arg1 = get_arg(args, 2); const arg2 = get_arg(args, 3); - const arg3 = get_arg(args, 4); - const arg4 = get_arg(args, 5); set_arg_intptr(arg1, mono_wasm_main_thread_ptr() as any); - set_arg_i32(arg2, jsThreadBlockingMode); - set_arg_i32(arg3, jsThreadInteropMode); - set_arg_i32(arg4, mainThreadingMode); + + // sync with JSHostImplementation.Types.cs + switch (jsThreadBlockingMode) { + case JSThreadBlockingMode.PreventSynchronousJSExport: + set_arg_i32(arg2, 0); + break; + case JSThreadBlockingMode.ThrowWhenBlockingWait: + set_arg_i32(arg2, 1); + break; + case JSThreadBlockingMode.WarnWhenBlockingWait: + set_arg_i32(arg2, 2); + break; + case JSThreadBlockingMode.DangerousAllowBlockingWait: + set_arg_i32(arg2, 100); + break; + default: + throw new Error("Invalid jsThreadBlockingMode"); + } // this block is like invoke_sync_jsexport() but without assert_js_interop() cwraps.mono_wasm_invoke_jsexport(managedExports.InstallMainSynchronizationContext!, args); @@ -281,10 +296,12 @@ export function invoke_sync_jsexport (method: MonoMethod, args: JSMarshalerArgum if (!WasmEnableThreads) { cwraps.mono_wasm_invoke_jsexport(method, args as any); } else { - if (runtimeHelpers.config.jsThreadInteropMode == JSThreadInteropMode.NoSyncJSInterop) { - throw new Error("Cannot call synchronous C# methods."); - } else if (runtimeHelpers.isPendingSynchronousCall) { - throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + if (monoThreadInfo.isUI) { + if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) { + throw new Error("Cannot call synchronous C# methods."); + } else if (runtimeHelpers.isPendingSynchronousCall) { + throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method."); + } } if (runtimeHelpers.isManagedRunningOnCurrentThread) { twraps.mono_wasm_invoke_jsexport_sync(method, args as any); diff --git a/src/mono/browser/runtime/multi-threading.md b/src/mono/browser/runtime/multi-threading.md deleted file mode 100644 index e4b3985923d50..0000000000000 --- a/src/mono/browser/runtime/multi-threading.md +++ /dev/null @@ -1,52 +0,0 @@ -# Multi-threading with JavaScript interop - -## Meaningful configurations are: - - * Single-threaded mode as you know it since .Net 6 - - default, safe, tested, supported - - from .Net 8 it could be easily started also as a web worker, but you need your own messaging between main and worker - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + **default threading**, safe, tested, supported - + blocking `.Wait` is allowed on thread pool and new threads - - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - - * `MainThreadingMode.DeputyAndIOThreads` + `JSThreadBlockingMode.AllowBlockingWaitInAsyncCode` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + **default threading**, safe, tested, supported - + blocking `.Wait` is allowed on thread pool and new threads - - blocking `.Wait` throws `PlatformNotSupportedException` on `JSWebWorker` and main thread only when they are called from JS via synchronous `JSExport` - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.AllowBlockingWait` + `JSThreadInteropMode.SimpleSynchronousJSInterop` - + pragmatic for legacy codebase, which contains blocking code and can't be fully executed on thread pool or new threads - - ** could cause deadlocks !!!** - - Use your own judgment before you opt in. - - blocking .Wait is allowed on all threads! - - blocking .Wait on pending JS `Task`/`Promise` (like HTTP/WS requests) could cause deadlocks! - - reason is that blocked thread can't process the browser event loop - - so it can't resolve the promises - - even when it's longer `Promise`/`Task` chain - - DOM events like `onClick` need to be asynchronous, if the handler needs use synchronous `[JSImport]` - - synchronous calls to `[JSImport]`/`[JSExport]` can't synchronously call back - -## Unsupported combinations are: - * `MainThreadingMode.DeputyThread` + `JSThreadBlockingMode.NoBlockingWait` + `JSThreadInteropMode.NoSyncJSInterop` - + very safe - - HTTP/WS requests are not possible because it currently uses synchronous JS interop - - Blazor doesn't work because it currently uses synchronous JS interop - * `MainThreadingMode.UIThread` - - not recommended, not tested, not supported! - - can deadlock on creating new threads - - can deadlock on blocking `.Wait` for a pending JS `Promise`/`Task`, including HTTP/WS requests - - .Wait is spin-waiting - it blocks debugger, network, UI rendering, ... - + JS interop to UI is faster, synchronous and re-entrant - -### There could be more JSThreadInteropModes: - - allow re-entrant synchronous JS interop on `JSWebWorker`. - - This is possible because managed code is running on same thread as JS. - - But it's nuanced to debug it, when things go wrong. - - allow re-entrant synchronous JS interop also on deputy thread. - - This is not possible for deputy, because it would deadlock on call back to different thread. - - The thread receiving the callback is still blocked waiting for the first synchronous call to finish. diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts index 195df3e126ab6..3678d709d1050 100644 --- a/src/mono/browser/runtime/pthreads/index.ts +++ b/src/mono/browser/runtime/pthreads/index.ts @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { mono_log_warn } from "../logging"; +import { utf16ToString } from "../strings"; + export { mono_wasm_main_thread_ptr, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop, mono_wasm_pthread_ptr, update_thread_info, isMonoThreadMessage, monoThreadInfo, @@ -18,3 +21,8 @@ export { export { mono_wasm_start_deputy_thread_async } from "./deputy-thread"; export { mono_wasm_start_io_thread_async } from "./io-thread"; + +export function mono_wasm_warn_about_blocking_wait (ptr: number, length: number) { + const warning = utf16ToString(ptr, ptr + (length * 2)); + mono_log_warn(warning); +} diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 2ab5110ec4687..316b9a96135a2 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -4,7 +4,7 @@ import WasmEnableThreads from "consts:wasmEnableThreads"; import BuildConfiguration from "consts:configuration"; -import { DotnetModuleInternal, CharPtrNull, MainThreadingMode } from "./types/internal"; +import { DotnetModuleInternal, CharPtrNull } from "./types/internal"; import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, createPromiseController, mono_assert } from "./globals"; import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; @@ -274,19 +274,14 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) mono_log_info("UI thread is alive!"); }, 3000); - if (WasmEnableThreads && - (runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyThread - || runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads)) { + if (WasmEnableThreads) { // this will create thread and call start_runtime() on it runtimeHelpers.monoThreadInfo = monoThreadInfo; runtimeHelpers.isManagedRunningOnCurrentThread = false; update_thread_info(); runtimeHelpers.managedThreadTID = tcwraps.mono_wasm_create_deputy_thread(); runtimeHelpers.proxyGCHandle = await runtimeHelpers.afterMonoStarted.promise; - - if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { - runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); - } + runtimeHelpers.ioThreadTID = tcwraps.mono_wasm_create_io_thread(); // TODO make UI thread not managed tcwraps.mono_wasm_register_ui_thread(); @@ -301,7 +296,7 @@ async function onRuntimeInitializedAsync (userOnRuntimeInitialized: () => void) await start_runtime(); } - if (WasmEnableThreads && runtimeHelpers.config.mainThreadingMode == MainThreadingMode.DeputyAndIOThreads) { + if (WasmEnableThreads) { await runtimeHelpers.afterIOStarted.promise; } @@ -539,10 +534,7 @@ export async function start_runtime () { monoThreadInfo.isRegistered = true; runtimeHelpers.currentThreadTID = monoThreadInfo.pthreadId = runtimeHelpers.managedThreadTID = mono_wasm_pthread_ptr(); update_thread_info(); - runtimeHelpers.proxyGCHandle = install_main_synchronization_context( - runtimeHelpers.config.jsThreadBlockingMode!, - runtimeHelpers.config.jsThreadInteropMode!, - runtimeHelpers.config.mainThreadingMode!); + runtimeHelpers.proxyGCHandle = install_main_synchronization_context(runtimeHelpers.config.jsThreadBlockingMode!); runtimeHelpers.isManagedRunningOnCurrentThread = true; // start finalizer thread, lazy diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index b333e254703b0..c9477ebae7b28 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -95,9 +95,7 @@ export type MonoConfigInternal = MonoConfig & { GitHash?: string, ProductVersion?: string, - mainThreadingMode?: MainThreadingMode, jsThreadBlockingMode?: JSThreadBlockingMode, - jsThreadInteropMode?: JSThreadInteropMode, }; export type RunArguments = { @@ -569,36 +567,37 @@ export interface MonoThreadMessage { cmd: string; } -// keep in sync with JSHostImplementation.Types.cs -export const enum MainThreadingMode { - // 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 comment - DeputyAndIOThreads = 2, -} - // keep in sync with JSHostImplementation.Types.cs export const enum JSThreadBlockingMode { - // 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 comment - 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 JSHostImplementation.Types.cs -export const enum JSThreadInteropMode { - // 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, + /** + * Prevents synchronous JSExport from being called from JavaScript code in UI thread. + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always warns. + * This is the default mode. + */ + PreventSynchronousJSExport = "PreventSynchronousJSExport", + /** + * Allows synchronous JSExport to be called from JavaScript code also in UI thread. + * Inside of that call blocking .Wait throws PNSE. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always throws PNSE. + */ + ThrowWhenBlockingWait = "ThrowWhenBlockingWait", + /** + * Allows synchronous JSExport to be called from JavaScript code also in UI thread. + * Inside of that call blocking .Wait warns. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + * On JSWebWorker synchronous JSExport always works. + * On JSWebWorker blocking .Wait always warns. + */ + WarnWhenBlockingWait = "WarnWhenBlockingWait", + /** + * Allows synchronous JSExport to be called from JavaScript code, and allows managed code to use blocking .Wait + * .Wait on Promise/Task chains could lead to deadlock because JS event loop is not processed and it can't resolve JS promises. + * This mode is dangerous and not supported. + * Allows synchronous JSExport to be called from JavaScript code also in Main thread. + * Inside of that call nested call back to synchronous JSImport throws PNSE (because it would deadlock otherwise in 100% cases). + */ + DangerousAllowBlockingWait = "DangerousAllowBlockingWait", } diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js index 3aacd8e2c67d6..1feb21ef2f796 100644 --- a/src/mono/browser/test-main.js +++ b/src/mono/browser/test-main.js @@ -252,7 +252,8 @@ function configureRuntime(dotnet, runArgs) { .withInteropCleanupOnExit() .withDumpThreadsOnNonZeroExit() .withConfig({ - loadAllSatelliteResources: true + loadAllSatelliteResources: true, + jsThreadBlockingMode: "ThrowWhenBlockingWait", }); if (ENVIRONMENT_IS_NODE) { diff --git a/src/mono/sample/wasm/browser-threads/Program.cs b/src/mono/sample/wasm/browser-threads/Program.cs index 331f8e35de3cc..8783ace18be7b 100644 --- a/src/mono/sample/wasm/browser-threads/Program.cs +++ b/src/mono/sample/wasm/browser-threads/Program.cs @@ -38,9 +38,14 @@ public static async Task Main(string[] args) public static void Progress2() { // both calls here are sync POSIX calls dispatched to UI thread, which is already blocked because this is synchronous method on deputy thread - // in should not deadlock anyway, see also invoke_later_when_on_ui_thread_sync and emscripten_yield + // it should not deadlock anyway, see also invoke_later_when_on_ui_thread_sync and emscripten_yield var cwd = Directory.GetCurrentDirectory(); Console.WriteLine("Progress! "+ cwd); + + // below is blocking call, which means that UI will spin-lock little longer + // it will warn about blocking wait because of jsThreadBlockingMode: "WarnWhenBlockingWait" + // but it will not deadlock because underlying task chain is not JS promise + Task.Delay(10).Wait(); } [JSExport] diff --git a/src/mono/sample/wasm/browser-threads/main.js b/src/mono/sample/wasm/browser-threads/main.js index 8da1e4fb608e6..ea97a5ce200c8 100644 --- a/src/mono/sample/wasm/browser-threads/main.js +++ b/src/mono/sample/wasm/browser-threads/main.js @@ -17,6 +17,9 @@ try { .withElementOnExit() .withExitCodeLogging() .withExitOnUnhandledError() + .withConfig({ + jsThreadBlockingMode: "WarnWhenBlockingWait", + }) .create(); setModuleImports("main.js", {