-
Notifications
You must be signed in to change notification settings - Fork 5.2k
[wasm-mt] Support async JS interop on threadpool threads #84494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
8c3fef3
a3db886
847ecd5
dd69c8f
451d21b
2889a1f
92d80a5
5f4acc8
806b770
d2811f7
48d22ae
c813701
0a4dd8c
77a54ff
fd99953
4671e03
e22ca46
c591501
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| { | ||
| /// <summary> | ||
| /// The worker thread infastructure for the CLR thread pool. | ||
| /// </summary> | ||
| private static partial class WorkerThread | ||
| { | ||
| /// <summary> | ||
| /// Semaphore for controlling how many threads are currently working. | ||
| /// </summary> | ||
| 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<LowLevelLifoAsyncWaitSemaphore, object?> s_WorkLoopSemaphoreSuccess = new(WorkLoopSemaphoreSuccess); | ||
| private static readonly Action<LowLevelLifoAsyncWaitSemaphore, object?> 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); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the G_UNLIKELY here important or necessary? Just curious
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither - this code isn't particularly hot (I think), so it's more for documentation than anything else.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is also a hint to compilers that supports it to put the branch out of line assisting the branch predictors heuristics, so it should then end up with a conditional jmp to an out of line code block when true, but branch predictor will predict it to not be taken (normal default heuristics for forward branches)., potentially improving execution speed around that block. |
||
| /* 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,16 @@ 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 ()) | ||
| return 0; | ||
| } | ||
|
|
||
| mono_thread_info_exit (res); | ||
|
|
||
| g_assert_not_reached (); | ||
|
|
@@ -1355,6 +1371,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 +4930,15 @@ 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 defined(HOST_BROWSER) && !defined(DISABLE_THREADS) | ||
| // HACK: threadpool threads can return to the JS event loop | ||
| // WISH: support this for other threads, too | ||
|
||
| if (internal->threadpool_thread) | ||
| create_flags |= MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP; | ||
| #endif | ||
|
|
||
| res = create_thread (internal, internal, NULL, NULL, stack_size, create_flags, error); | ||
| if (!res) { | ||
| UNLOCK_THREAD (internal); | ||
| return; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.