diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 7859d2b8ccff9..b27df31bb007a 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2530,9 +2530,9 @@ - + - + diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs index 39e1d6453263e..a9c4e038129a4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Portable.cs @@ -19,7 +19,9 @@ public static partial class ThreadPool { // Indicates whether the thread pool should yield the thread from the dispatch loop to the runtime periodically so that // the runtime may use the thread for processing other work +#if !(TARGET_BROWSER && FEATURE_WASM_THREADS) internal static bool YieldFromDispatchLoop => false; +#endif #if NATIVEAOT private const bool IsWorkerTrackingEnabledInConfig = false; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs index 38605743fad93..c15b81826b0fb 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Interop/LegacyExports.cs @@ -32,18 +32,15 @@ internal static void PreventTrimming() public static void GetCSOwnedObjectByJSHandleRef(nint jsHandle, int shouldAddInflight, out JSObject? result) { - lock (JSHostImplementation.s_csOwnedObjects) + if (JSHostImplementation.ThreadCsOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference)) { - if (JSHostImplementation.s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference)) + reference.TryGetTarget(out JSObject? jsObject); + if (shouldAddInflight != 0) { - reference.TryGetTarget(out JSObject? jsObject); - if (shouldAddInflight != 0) - { - jsObject?.AddInFlight(); - } - result = jsObject; - return; + jsObject?.AddInFlight(); } + result = jsObject; + return; } result = null; } @@ -77,14 +74,12 @@ public static void CreateCSOwnedProxyRef(nint jsHandle, LegacyHostImplementation JSObject? res = null; - lock (JSHostImplementation.s_csOwnedObjects) + if (!JSHostImplementation.ThreadCsOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference) || + !reference.TryGetTarget(out res) || + res.IsDisposed) { - if (!JSHostImplementation.s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference) || - !reference.TryGetTarget(out res) || - res.IsDisposed) - { #pragma warning disable CS0612 // Type or member is obsolete - res = mappedType switch + res = mappedType switch { LegacyHostImplementation.MappedType.JSObject => new JSObject(jsHandle), LegacyHostImplementation.MappedType.Array => new Array(jsHandle), @@ -95,8 +90,7 @@ public static void CreateCSOwnedProxyRef(nint jsHandle, LegacyHostImplementation _ => throw new ArgumentOutOfRangeException(nameof(mappedType)) }; #pragma warning restore CS0612 // Type or member is obsolete - JSHostImplementation.s_csOwnedObjects[(int)jsHandle] = new WeakReference(res, trackResurrection: true); - } + JSHostImplementation.ThreadCsOwnedObjects[(int)jsHandle] = new WeakReference(res, trackResurrection: true); } if (shouldAddInflight != 0) { diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs index 7738bb834fc21..b439586d21301 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSHostImplementation.cs @@ -15,7 +15,20 @@ internal static partial class JSHostImplementation private const string TaskGetResultName = "get_Result"; private static MethodInfo? s_taskGetResultMethodInfo; // we use this to maintain identity of JSHandle for a JSObject proxy - public static readonly Dictionary> s_csOwnedObjects = new Dictionary>(); +#if FEATURE_WASM_THREADS + [ThreadStatic] +#endif + private static Dictionary>? s_csOwnedObjects; + + public static Dictionary> ThreadCsOwnedObjects + { + get + { + s_csOwnedObjects ??= new (); + return s_csOwnedObjects; + } + } + // we use this to maintain identity of GCHandle for a managed object public static Dictionary s_gcHandleFromJSOwnedObject = new Dictionary(ReferenceEqualityComparer.Instance); @@ -24,10 +37,7 @@ public static void ReleaseCSOwnedObject(nint jsHandle) { if (jsHandle != IntPtr.Zero) { - lock (s_csOwnedObjects) - { - s_csOwnedObjects.Remove((int)jsHandle); - } + ThreadCsOwnedObjects.Remove((int)jsHandle); Interop.Runtime.ReleaseCSOwnedObject(jsHandle); } } @@ -175,17 +185,14 @@ public static unsafe void FreeMethodSignatureBuffer(JSFunctionBinding signature) public static JSObject CreateCSOwnedProxy(nint jsHandle) { - JSObject? res = null; + JSObject? res; - lock (s_csOwnedObjects) + if (!ThreadCsOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference) || + !reference.TryGetTarget(out res) || + res.IsDisposed) { - if (!s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference? reference) || - !reference.TryGetTarget(out res) || - res.IsDisposed) - { - res = new JSObject(jsHandle); - s_csOwnedObjects[(int)jsHandle] = new WeakReference(res, trackResurrection: true); - } + res = new JSObject(jsHandle); + ThreadCsOwnedObjects[(int)jsHandle] = new WeakReference(res, trackResurrection: true); } return res; } diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs index 6a5de6a03e503..661b21690670a 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/Legacy/LegacyHostImplementation.cs @@ -21,10 +21,7 @@ public static void ReleaseInFlight(object obj) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RegisterCSOwnedObject(JSObject proxy) { - lock (JSHostImplementation.s_csOwnedObjects) - { - JSHostImplementation.s_csOwnedObjects[(int)proxy.JSHandle] = new WeakReference(proxy, trackResurrection: true); - } + JSHostImplementation.ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference(proxy, trackResurrection: true); } public static MarshalType GetMarshalTypeFromType(Type type) diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index a76cd02986cca..b8e53f4ed0fea 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -281,6 +281,9 @@ + + + diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..632b0c934ee4c --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Browser.Threads.Mono.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading; + +internal sealed partial class PortableThreadPool +{ + private static partial class WorkerThread + { + private static bool IsIOPending => WebWorkerEventLoop.HasJavaScriptInteropDependents; + } + + private struct CpuUtilizationReader + { +#pragma warning disable CA1822 + public double CurrentUtilization => 0.0; // FIXME: can we do better +#pragma warning restore CA1822 + } +} diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..b45dee7fa2fd6 --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; + +namespace System.Threading +{ + internal sealed partial class PortableThreadPool + { + /// + /// The worker thread infastructure for the CLR thread pool. + /// + private static partial class WorkerThread + { + /// + /// Semaphore for controlling how many threads are currently working. + /// + private static readonly LowLevelLifoAsyncWaitSemaphore s_semaphore = + new LowLevelLifoAsyncWaitSemaphore( + 0, + MaxPossibleThreadCount, + AppContextConfigHelper.GetInt32Config( + "System.Threading.ThreadPool.UnfairSemaphoreSpinLimit", + SemaphoreSpinCountDefault, + false), + onWait: () => + { + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadWait( + (uint)ThreadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + }); + + private static readonly ThreadStart s_workerThreadStart = WorkerThreadStart; + + private sealed record SemaphoreWaitState(PortableThreadPool ThreadPoolInstance, LowLevelLock ThreadAdjustmentLock, WebWorkerEventLoop.KeepaliveToken KeepaliveToken) + { + public bool SpinWait = true; + + public void ResetIteration() { + SpinWait = true; + } + } + + private static void WorkerThreadStart() + { + Thread.CurrentThread.SetThreadPoolWorkerThreadName(); + + PortableThreadPool threadPoolInstance = ThreadPoolInstance; + + if (NativeRuntimeEventSource.Log.IsEnabled()) + { + NativeRuntimeEventSource.Log.ThreadPoolWorkerThreadStart( + (uint)threadPoolInstance._separated.counts.VolatileRead().NumExistingThreads); + } + + LowLevelLock threadAdjustmentLock = threadPoolInstance._threadAdjustmentLock; + var keepaliveToken = WebWorkerEventLoop.KeepalivePush(); + SemaphoreWaitState state = new(threadPoolInstance, threadAdjustmentLock, keepaliveToken) { SpinWait = true }; + // set up the callbacks for semaphore waits, tell + // emscripten to keep the thread alive, and return to + // the JS event loop. + WaitForWorkLoop(s_semaphore, state); + // return from thread start with keepalive - the thread will stay alive in the JS event loop + } + + private static readonly Action s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); + private static readonly Action s_WorkLoopSemaphoreTimedOut = new(WorkLoopSemaphoreTimedOut); + + private static void WaitForWorkLoop(LowLevelLifoAsyncWaitSemaphore semaphore, SemaphoreWaitState state) + { + semaphore.PrepareAsyncWait(ThreadPoolThreadTimeoutMs, s_WorkLoopSemaphoreSuccess, s_WorkLoopSemaphoreTimedOut, state); + // thread should still be kept alive + Debug.Assert(state.KeepaliveToken.Valid); + } + + private static void WorkLoopSemaphoreSuccess(LowLevelLifoAsyncWaitSemaphore semaphore, object? stateObject) + { + SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; + WorkerDoWork(state.ThreadPoolInstance, ref state.SpinWait); + // Go around the loop one more time, keeping existing mutated state + WaitForWorkLoop(semaphore, state); + } + + private static void WorkLoopSemaphoreTimedOut(LowLevelLifoAsyncWaitSemaphore semaphore, object? stateObject) + { + SemaphoreWaitState state = (SemaphoreWaitState)stateObject!; + if (ShouldExitWorker(state.ThreadPoolInstance, state.ThreadAdjustmentLock)) { + // we're done, kill the thread. + + // we're wrapped in an emscripten eventloop handler which will consult the + // keepalive count, destroy the thread and run the TLS dtor which will + // unregister the thread from Mono + state.KeepaliveToken.Pop(); + return; + } else { + // more work showed up while we were shutting down, go around one more time + state.ResetIteration(); + WaitForWorkLoop(semaphore, state); + } + } + + private static void CreateWorkerThread() + { + // Thread pool threads must start in the default execution context without transferring the context, so + // using captureContext: false. + Thread workerThread = new Thread(s_workerThreadStart); + workerThread.IsThreadPoolThread = true; + workerThread.IsBackground = true; + // thread name will be set in thread proc + + // This thread will return to the JS event loop - tell the runtime not to cleanup + // after the start function returns, if the Emscripten keepalive is non-zero. + WebWorkerEventLoop.StartExitable(workerThread, captureContext: false); + } + } + } +} diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index 31bc824008607..d5b1918608b77 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs @@ -37,6 +37,7 @@ public partial class Thread private int interruption_requested; private IntPtr longlived; internal bool threadpool_thread; + internal bool external_eventloop; // browser-wasm: thread will return to the JS eventloop /* These are used from managed code */ internal byte apartment_state; internal int managed_id; @@ -352,5 +353,17 @@ private static void SpinWait_nop() private static extern void SetPriority(Thread thread, int priority); internal int GetSmallId() => small_id; + + internal bool HasExternalEventLoop + { + get + { + return external_eventloop; + } + set + { + external_eventloop = value; + } + } } } diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs new file mode 100644 index 0000000000000..7933e49db422b --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Threading/ThreadPool.Browser.Threads.Mono.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading +{ + public static partial class ThreadPool + { + // Indicates that the threadpool should yield the thread from the dispatch loop to the + // runtime periodically. We use this to return back to the JS event loop so that the JS + // event queue can be drained + internal static bool YieldFromDispatchLoop => true; + } +} diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs index 5a49c076271d1..73c2959293d52 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/WebWorkerEventLoop.Browser.Threads.Mono.cs @@ -67,10 +67,10 @@ internal static void StartExitable(Thread thread, bool captureContext) // not needed by PortableThreadPool.WorkerThread if (captureContext) throw new InvalidOperationException(); - // hack: threadpool threads are exitable, and nothing else is. - // see create_thread() in mono/metadata/threads.c + // for now, threadpool threads are exitable, and nothing else is. if (!thread.IsThreadPoolThread) throw new InvalidOperationException(); + thread.HasExternalEventLoop = true; thread.UnsafeStart(); } diff --git a/src/mono/mono/metadata/object-internals.h b/src/mono/mono/metadata/object-internals.h index be4ce486c91a7..b968762c210cc 100644 --- a/src/mono/mono/metadata/object-internals.h +++ b/src/mono/mono/metadata/object-internals.h @@ -616,6 +616,7 @@ struct _MonoInternalThread { * longer */ MonoLongLivedThreadData *longlived; MonoBoolean threadpool_thread; + MonoBoolean external_eventloop; guint8 apartment_state; gint32 managed_id; guint32 small_id; diff --git a/src/mono/mono/metadata/threads-types.h b/src/mono/mono/metadata/threads-types.h index fe57d74a02e39..5e89f84bef211 100644 --- a/src/mono/mono/metadata/threads-types.h +++ b/src/mono/mono/metadata/threads-types.h @@ -78,6 +78,10 @@ typedef enum { MONO_THREAD_CREATE_FLAGS_DEBUGGER = 0x02, MONO_THREAD_CREATE_FLAGS_FORCE_CREATE = 0x04, MONO_THREAD_CREATE_FLAGS_SMALL_STACK = 0x08, + // "external eventloop" means the thread main function can return without killing the thread + // and the thread will continue to be attached to the runtime and may invoke embedding APIs + // and managed calls. There is usually some platform-specific way to shut down the thread. + MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP = 0x10, } MonoThreadCreateFlags; MONO_COMPONENT_API MonoInternalThread* diff --git a/src/mono/mono/metadata/threads.c b/src/mono/mono/metadata/threads.c index 08dc87fd8a62c..0788d9ccdac13 100644 --- a/src/mono/mono/metadata/threads.c +++ b/src/mono/mono/metadata/threads.c @@ -1088,6 +1088,7 @@ typedef struct { MonoThreadStart start_func; gpointer start_func_arg; gboolean force_attach; + gboolean external_eventloop; gboolean failed; MonoCoopSem registered; } StartInfo; @@ -1173,6 +1174,8 @@ start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) /* Let the thread that called Start() know we're ready */ mono_coop_sem_post (&start_info->registered); + gboolean external_eventloop = start_info->external_eventloop; + if (mono_atomic_dec_i32 (&start_info->ref) == 0) { mono_coop_sem_destroy (&start_info->registered); g_free (start_info); @@ -1240,6 +1243,12 @@ start_wrapper_internal (StartInfo *start_info, gsize *stack_ptr) THREAD_DEBUG (g_message ("%s: (%" G_GSIZE_FORMAT ") Start wrapper terminating", __func__, mono_native_thread_id_get ())); + if (G_UNLIKELY (external_eventloop)) { + /* if the thread wants to stay alive in an external eventloop, don't clean up after it */ + if (mono_thread_platform_external_eventloop_keepalive_check ()) + return 0; + } + /* Do any cleanup needed for apartment state. This * cannot be done in mono_thread_detach_internal since * mono_thread_detach_internal could be called for a @@ -1266,9 +1275,19 @@ start_wrapper (gpointer data) info = mono_thread_info_attach (); info->runtime_thread = TRUE; + gboolean external_eventloop = start_info->external_eventloop; /* Run the actual main function of the thread */ res = start_wrapper_internal (start_info, (gsize*)info->stack_end); + if (G_UNLIKELY (external_eventloop)) { + /* if the thread wants to stay alive, don't clean up after it */ + if (mono_thread_platform_external_eventloop_keepalive_check ()) { + /* while we wait in the external eventloop, we're GC safe */ + MONO_REQ_GC_SAFE_MODE; + return 0; + } + } + mono_thread_info_exit (res); g_assert_not_reached (); @@ -1355,6 +1374,7 @@ create_thread (MonoThread *thread, MonoInternalThread *internal, MonoThreadStart start_info->start_func_arg = start_func_arg; start_info->force_attach = flags & MONO_THREAD_CREATE_FLAGS_FORCE_CREATE; start_info->failed = FALSE; + start_info->external_eventloop = (flags & MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP) != 0; mono_coop_sem_init (&start_info->registered, 0); if (flags != MONO_THREAD_CREATE_FLAGS_SMALL_STACK) @@ -4913,7 +4933,11 @@ ves_icall_System_Threading_Thread_StartInternal (MonoThreadObjectHandle thread_h return; } - res = create_thread (internal, internal, NULL, NULL, stack_size, MONO_THREAD_CREATE_FLAGS_NONE, error); + MonoThreadCreateFlags create_flags = MONO_THREAD_CREATE_FLAGS_NONE; + if (G_UNLIKELY (internal->external_eventloop)) + create_flags |= MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP; + + res = create_thread (internal, internal, NULL, NULL, stack_size, create_flags, error); if (!res) { UNLOCK_THREAD (internal); return; diff --git a/src/mono/mono/utils/mono-threads-posix.c b/src/mono/mono/utils/mono-threads-posix.c index 08e843cdd7023..a70691d01e8fe 100644 --- a/src/mono/mono/utils/mono-threads-posix.c +++ b/src/mono/mono/utils/mono-threads-posix.c @@ -133,6 +133,15 @@ mono_threads_platform_exit (gsize exit_code) pthread_exit ((gpointer) exit_code); } +gboolean +mono_thread_platform_external_eventloop_keepalive_check (void) +{ + /* vanilla POSIX thread creation doesn't support an external eventloop: when the thread main + function returns, the thread is done. + */ + return FALSE; +} + #if HOST_FUCHSIA int mono_thread_info_get_system_max_stack_size (void) diff --git a/src/mono/mono/utils/mono-threads-wasm.c b/src/mono/mono/utils/mono-threads-wasm.c index 96e5446388a43..6a33dfa0d5bc2 100644 --- a/src/mono/mono/utils/mono-threads-wasm.c +++ b/src/mono/mono/utils/mono-threads-wasm.c @@ -301,6 +301,20 @@ mono_thread_platform_create_thread (MonoThreadStart thread_fn, gpointer thread_d #endif } +gboolean +mono_thread_platform_external_eventloop_keepalive_check (void) +{ +#if defined(HOST_BROWSER) && !defined(DISABLE_THREADS) + /* if someone called emscripten_runtime_keepalive_push (), the + * thread will stay alive in the JS event loop after returning + * from the thread's main function. + */ + return emscripten_runtime_keepalive_check (); +#else + return FALSE; +#endif +} + void mono_threads_platform_init (void) { } diff --git a/src/mono/mono/utils/mono-threads-windows.c b/src/mono/mono/utils/mono-threads-windows.c index 3e56205c0ab88..169449b831e83 100644 --- a/src/mono/mono/utils/mono-threads-windows.c +++ b/src/mono/mono/utils/mono-threads-windows.c @@ -501,6 +501,15 @@ typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); static gboolean is_wow64 = FALSE; #endif +gboolean +mono_thread_platform_external_eventloop_keepalive_check (void) +{ + /* We don't support thread creation with an external eventloop on WIN32: when the thread start + function returns, the thread is done. + */ + return FALSE; +} + /* We do this at init time to avoid potential races with module opening */ void mono_threads_platform_init (void) diff --git a/src/mono/mono/utils/mono-threads.h b/src/mono/mono/utils/mono-threads.h index 6a548b1838c34..cdfc9f6133671 100644 --- a/src/mono/mono/utils/mono-threads.h +++ b/src/mono/mono/utils/mono-threads.h @@ -632,6 +632,9 @@ gboolean mono_threads_platform_in_critical_region (THREAD_INFO_TYPE *info); gboolean mono_threads_platform_yield (void); void mono_threads_platform_exit (gsize exit_code); +gboolean +mono_thread_platform_external_eventloop_keepalive_check (void); + void mono_threads_coop_begin_global_suspend (void); void mono_threads_coop_end_global_suspend (void); diff --git a/src/mono/sample/wasm/browser-threads-minimal/Program.cs b/src/mono/sample/wasm/browser-threads-minimal/Program.cs index 0b9784836bbd7..4379c9092bf61 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Program.cs +++ b/src/mono/sample/wasm/browser-threads-minimal/Program.cs @@ -18,6 +18,77 @@ public static int Main(string[] args) return 0; } + [JSExport] + public static async Task TestCanStartThread() + { + var tcs = new TaskCompletionSource(); + var t = new Thread(() => + { + var childTid = Thread.CurrentThread.ManagedThreadId; + tcs.SetResult(childTid); + }); + t.Start(); + var childTid = await tcs.Task; + t.Join(); + if (childTid == Thread.CurrentThread.ManagedThreadId) + throw new Exception("Child thread ran on same thread as parent"); + } + + [JSImport("globalThis.setTimeout")] + static partial void GlobalThisSetTimeout([JSMarshalAs] Action cb, int timeoutMs); + + [JSImport("globalThis.fetch")] + private static partial Task GlobalThisFetch(string url); + + [JSExport] + public static async Task TestCallSetTimeoutOnWorker() + { + var t = Task.Run(TimeOutThenComplete); + await t; + Console.WriteLine ($"XYZ: Main Thread caught task tid:{Thread.CurrentThread.ManagedThreadId}"); + } + + const string fetchhelper = "./fetchelper.js"; + + [JSImport("responseText", fetchhelper)] + private static partial Task FetchHelperResponseText(JSObject response); + + [JSExport] + public static async Task FetchBackground(string url) + { + var t = Task.Run(async () => + { + using var import = await JSHost.ImportAsync(fetchhelper, "./fetchhelper.js"); + var r = await GlobalThisFetch(url); + var ok = (bool)r.GetPropertyAsBoolean("ok"); + + Console.WriteLine($"XYZ: FetchBackground fetch returned to thread:{Thread.CurrentThread.ManagedThreadId}, ok: {ok}"); + if (ok) + { + var text = await FetchHelperResponseText(r); + Console.WriteLine($"XYZ: FetchBackground fetch returned to thread:{Thread.CurrentThread.ManagedThreadId}, text: {text}"); + return text; + } + return "not-ok"; + }); + var r = await t; + Console.WriteLine($"XYZ: FetchBackground thread:{Thread.CurrentThread.ManagedThreadId} background thread returned"); + return r; + } + + private static async Task TimeOutThenComplete() + { + var tcs = new TaskCompletionSource(); + Console.WriteLine ($"XYZ: Task running tid:{Thread.CurrentThread.ManagedThreadId}"); + GlobalThisSetTimeout(() => { + tcs.SetResult(); + Console.WriteLine ($"XYZ: Timeout fired tid:{Thread.CurrentThread.ManagedThreadId}"); + }, 250); + Console.WriteLine ($"XYZ: Task sleeping tid:{Thread.CurrentThread.ManagedThreadId}"); + await tcs.Task; + Console.WriteLine ($"XYZ: Task resumed tid:{Thread.CurrentThread.ManagedThreadId}"); + } + [JSExport] public static async Task RunBackgroundThreadCompute() { @@ -41,10 +112,27 @@ public static async Task RunBackgroundLongRunningTaskCompute() return await t; } + [JSExport] + public static async Task RunBackgroundTaskRunCompute() + { + var t1 = Task.Run (() => { + var n = CountingCollatzTest(); + return n; + }); + var t2 = Task.Run (() => { + var n = CountingCollatzTest(); + return n; + }); + var rs = await Task.WhenAll (new [] { t1, t2 }); + if (rs[0] != rs[1]) + throw new Exception ($"Results from two tasks {rs[0]}, {rs[1]}, differ"); + return rs[0]; + } + public static int CountingCollatzTest() { const int limit = 5000; - const int maxInput = 500_000; + const int maxInput = 200_000; int bigly = 0; int hugely = 0; int maxSteps = 0; @@ -60,7 +148,7 @@ public static int CountingCollatzTest() Console.WriteLine ($"Bigly: {bigly}, Hugely: {hugely}, maxSteps: {maxSteps}"); - if (bigly == 241677 && hugely == 0 && maxSteps == 448) + if (bigly == 86187 && hugely == 0 && maxSteps == 382) return 524; else return 0; diff --git a/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj b/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj index f9c81f4b40e71..defce7521ac7f 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj +++ b/src/mono/sample/wasm/browser-threads-minimal/Wasm.Browser.Threads.Minimal.Sample.csproj @@ -6,6 +6,8 @@ + + diff --git a/src/mono/sample/wasm/browser-threads-minimal/blurst.txt b/src/mono/sample/wasm/browser-threads-minimal/blurst.txt new file mode 100644 index 0000000000000..6679d914da1c7 --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/blurst.txt @@ -0,0 +1 @@ +It was the best of times, it was the blurst of times. diff --git a/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js new file mode 100644 index 0000000000000..928492378fc6c --- /dev/null +++ b/src/mono/sample/wasm/browser-threads-minimal/fetchhelper.js @@ -0,0 +1,11 @@ + +function delay(timeoutMs) { + return new Promise(resolve => setTimeout(resolve, timeoutMs)); +} + +export async function responseText(response) /* Promise */ { + console.log("artificially waiting for response for 25 seconds"); + await delay(25000); + console.log("artificial waiting done"); + return await response.text(); +} diff --git a/src/mono/sample/wasm/browser-threads-minimal/main.js b/src/mono/sample/wasm/browser-threads-minimal/main.js index f607d96c2846a..3179fd5739e5b 100644 --- a/src/mono/sample/wasm/browser-threads-minimal/main.js +++ b/src/mono/sample/wasm/browser-threads-minimal/main.js @@ -15,18 +15,41 @@ try { const exports = await getAssemblyExports(assemblyName); - const r1 = await exports.Sample.Test.RunBackgroundThreadCompute(); - if (r1 !== 524) { - const msg = `Unexpected result ${r1} from RunBackgroundThreadCompute()`; + console.log("smoke: running TestCanStartThread"); + await exports.Sample.Test.TestCanStartThread(); + console.log("smoke: TestCanStartThread done"); + + console.log ("smoke: running TestCallSetTimeoutOnWorker"); + await exports.Sample.Test.TestCallSetTimeoutOnWorker(); + console.log ("smoke: TestCallSetTimeoutOnWorker done"); + + console.log ("smoke: running FetchBackground(blurst.txt)"); + let s = await exports.Sample.Test.FetchBackground("./blurst.txt"); + console.log ("smoke: FetchBackground(blurst.txt) done"); + if (s !== "It was the best of times, it was the blurst of times.\n") { + const msg = `Unexpected FetchBackground result ${s}`; document.getElementById("out").innerHTML = msg; - throw new Error(msg); + throw new Error (msg); + } + + console.log ("smoke: running FetchBackground(missing)"); + s = await exports.Sample.Test.FetchBackground("./missing.txt"); + console.log ("smoke: FetchBackground(missing) done"); + if (s !== "not-ok") { + const msg = `Unexpected FetchBackground(missing) result ${s}`; + document.getElementById("out").innerHTML = msg; + throw new Error (msg); } - const r2 = await exports.Sample.Test.RunBackgroundLongRunningTaskCompute(); - if (r2 !== 524) { - const msg = `Unexpected result ${r2} from RunBackgorundLongRunningTaskCompute()`; + + console.log ("smoke: running TaskRunCompute"); + const r1 = await exports.Sample.Test.RunBackgroundTaskRunCompute(); + if (r1 !== 524) { + const msg = `Unexpected result ${r1} from RunBackgorundTaskRunCompute()`; document.getElementById("out").innerHTML = msg; throw new Error(msg); } + console.log ("smoke: TaskRunCompute done"); + let exit_code = await runMain(assemblyName, []); exit(exit_code); diff --git a/src/mono/wasm/runtime/pthreads/worker/index.ts b/src/mono/wasm/runtime/pthreads/worker/index.ts index df8dbbb723e68..5669377e88b4c 100644 --- a/src/mono/wasm/runtime/pthreads/worker/index.ts +++ b/src/mono/wasm/runtime/pthreads/worker/index.ts @@ -115,7 +115,8 @@ function onMonoConfigReceived(config: MonoConfigInternal): void { export function mono_wasm_pthread_on_pthread_attached(pthread_id: pthread_ptr): void { const self = pthread_self; mono_assert(self !== null && self.pthread_id == pthread_id, "expected pthread_self to be set already when attaching"); - console.debug("MONO_WASM: attaching pthread to runtime", pthread_id); + if (runtimeHelpers.diagnosticTracing) + console.debug("MONO_WASM: attaching pthread to runtime 0x" + pthread_id.toString(16)); preRunWorker(); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadAttached, self)); } @@ -128,7 +129,8 @@ export function afterThreadInitTLS(): void { if (ENVIRONMENT_IS_PTHREAD) { const pthread_ptr = (Module)["_pthread_self"](); mono_assert(!is_nullish(pthread_ptr), "pthread_self() returned null"); - console.debug("MONO_WASM: after thread init, pthread ptr", pthread_ptr); + if (runtimeHelpers.diagnosticTracing) + console.debug("MONO_WASM: after thread init, pthread ptr 0x" + pthread_ptr.toString(16)); const self = setupChannelToMainThread(pthread_ptr); currentWorkerThreadEvents.dispatchEvent(makeWorkerThreadEvent(dotnetPthreadCreated, self)); } diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 8f84d6edba845..a6d20cd699a85 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -782,7 +782,8 @@ export async function configureWorkerStartup(module: DotnetModuleInternal): Prom pthreads_worker.setupPreloadChannelToMainThread(); // This is a good place for subsystems to attach listeners for pthreads_worker.currentWorkerThreadEvents pthreads_worker.currentWorkerThreadEvents.addEventListener(pthreads_worker.dotnetPthreadCreated, (ev) => { - console.debug("MONO_WASM: pthread created", ev.pthread_self.pthread_id); + if (runtimeHelpers.diagnosticTracing) + console.debug("MONO_WASM: pthread created 0x" + ev.pthread_self.pthread_id.toString(16)); }); // these are the only events which are called on worker