未验证 提交 7ddc45a4 编写于 作者: A Aleksey Kliger (λgeek) 提交者: GitHub

[wasm-mt] Support async JS interop on threadpool threads (#84494)

This is the last part of #84489 - initial runtime support for async JS interop on threadpool threads in multi-threaded WebAssembly.

Conceptually there are a few things here:

* A mechanism for the native runtime to start some threads as "exitable" so that we don't clean up when they return from their thread start function. (we will clean up when the pthreads TLS dtor runs)
* A change to make JSHostImplementation.s_csOwnedObjects a thread-static in multithreaded builds. This is because the map keys on a small integer handle that is allocated per-thread on the JS side as a handle for JS objects that are to be kept alive because managed code depends on them. (This is needed in general, but also makes the new smoke test work)
* A version of the PortableThreadPool.WorkerThread that starts as an exitable thread and uses asynchronous callbacks to wait for available threadpool work items and returns to the JS event loop periodically in order to allow JS promises to settle.
* A smoke test that does a background JS fetch and some async callbacks on it.

---


* [wasm-mt] Enable JSInterop on threadpool workers

   Implement PortableThreadPool loop using semaphore callbacks

   manage emscripten event loop from PortableThreadPool.WorkerThread

   make sure to keep the thread alive after setting up the semaphore wait.
   Cleanup the thread when exiting

   minimal sample - fetch on a background thread works

   Add WebWorkerEventLoop internal class to managed event loop keepalive

   Start threadpool threads with keepalive checks

   Add a flag to mono's thread start wrappers to keep track of threads that may not want cleanup to run after the Start function returns.

   Use the flag when starting threadpool threads.

   make minimal FetchBackground sample more like a unit test

   Set WorkerThread.IsIOPending when the current thread has unsettled JS interop promises.

   When IsIOPending is true, the worker will not exit even if it has no more work to do.  Instead it will repeatedly wait for more work to arrive or for all promises to settle.

   change minimal sample's fetch helper to artificially delay

   the delay is longer that the threadpool worker's semaphore timeout, in order to validate that the worker stays alive while there are unsettled promises

* [wasm-mt] Add background interop to smoketest

* update to use the LowLevelLifoAsyncWaitSemaphore

* adjust to renamed PortableThreadPool helper methods

* adjust to renamed WebWorkerEventLoop.HasJavaScriptInteropDependents

* extend and rationalize the smoke test a bit

   Add a test that just starts a thread and asserts that it has a different thread ID than the main thread.  This should allow us to rule out accidentally having the test pass on a single-threaded runtime

* hide some debug output

* smoke test: dispose of the ImportAsync result after the task is done

* [wasm-mt] make JSHostImplementation.s_csOwnedObjects ThreadStatic

   The integer jsHandles are not global - they are essentially indices into a JS array. So the mapping from a jsHandle to a JSObject must be per-thread.

   This fixes the thread affinity assertions in the smoketest (which were false positives - we looked up a worker's jsHandle and got back the main thread's JSObject - and then asserted that it was accessed from the wrong thread)

* remove locking on JSHostImplementation.CsOwnedObjects

   In single-threaded wasm, there is no need to lock since there is only one caller at a time.

   In multi-threaded wasm, the underlying dictionary is thread-static

* [threads] make the "external eventloop" platform independent

   It only does something on WASM, but in principle if other platforms allow us to run some code after returning from a thread start function, we could do it there, too.

* Add a Thread.HasExternalEventLoop managed property

   Set it from WebWorkerEventLoop.StartExitable.
   In native code, use it to set the `MONO_THREAD_CREATE_FLAGS_EXTERNAL_EVENTLOOP` flag when starting the thread.

* rename JSHostImplementation.ThreadCsOwnedObjects

   (used to be CsOwnedObjects)

   Rename to make it clear that it's objects owned by the current thread, not the runtime globally

* [checked] assert GC Safe mode, when returning to external eventloop
上级 47195755
......@@ -2530,9 +2530,9 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.ThreadCounts.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WaitThread.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerThread.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerThread.NonBrowser.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerThread.NonBrowser.cs" Condition="'$(TargetsBrowser)' != 'true'"/>
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerTracking.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.Unix.cs" Condition="'$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.Unix.cs" Condition="'$(TargetsUnix)' == 'true' or ('$(TargetsBrowser)' == 'true' and '$(FeatureWasmThreads)' != 'true') or '$(TargetsWasi)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\LowLevelLifoSemaphore.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\LowLevelLifoSemaphore.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
......
......@@ -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;
......
......@@ -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<JSObject>? reference))
{
if (JSHostImplementation.s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference<JSObject>? 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<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
if (!JSHostImplementation.s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference<JSObject>? 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<JSObject>(res, trackResurrection: true);
}
JSHostImplementation.ThreadCsOwnedObjects[(int)jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
if (shouldAddInflight != 0)
{
......
......@@ -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<int, WeakReference<JSObject>> s_csOwnedObjects = new Dictionary<int, WeakReference<JSObject>>();
#if FEATURE_WASM_THREADS
[ThreadStatic]
#endif
private static Dictionary<int, WeakReference<JSObject>>? s_csOwnedObjects;
public static Dictionary<int, WeakReference<JSObject>> ThreadCsOwnedObjects
{
get
{
s_csOwnedObjects ??= new ();
return s_csOwnedObjects;
}
}
// we use this to maintain identity of GCHandle for a managed object
public static Dictionary<object, IntPtr> s_gcHandleFromJSOwnedObject = new Dictionary<object, IntPtr>(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<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
if (!s_csOwnedObjects.TryGetValue((int)jsHandle, out WeakReference<JSObject>? reference) ||
!reference.TryGetTarget(out res) ||
res.IsDisposed)
{
res = new JSObject(jsHandle);
s_csOwnedObjects[(int)jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
res = new JSObject(jsHandle);
ThreadCsOwnedObjects[(int)jsHandle] = new WeakReference<JSObject>(res, trackResurrection: true);
}
return res;
}
......
......@@ -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<JSObject>(proxy, trackResurrection: true);
}
JSHostImplementation.ThreadCsOwnedObjects[(int)proxy.JSHandle] = new WeakReference<JSObject>(proxy, trackResurrection: true);
}
public static MarshalType GetMarshalTypeFromType(Type type)
......
......@@ -281,6 +281,9 @@
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' == 'true'">
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPoolBoundHandle.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\LowLevelLifoAsyncWaitSemaphore.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\PortableThreadPool.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\PortableThreadPool.WorkerThread.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\ThreadPool.Browser.Threads.Mono.cs" />
<Compile Include="$(BclSourcesRoot)\System\Threading\WebWorkerEventLoop.Browser.Threads.Mono.cs" />
</ItemGroup>
<ItemGroup Condition="('$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true') and '$(FeatureWasmThreads)' != 'true'">
......
// 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
}
}
// 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);
}
}
}
}
......@@ -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;
}
}
}
}
// 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;
}
}
......@@ -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();
}
......
......@@ -616,6 +616,7 @@ struct _MonoInternalThread {
* longer */
MonoLongLivedThreadData *longlived;
MonoBoolean threadpool_thread;
MonoBoolean external_eventloop;
guint8 apartment_state;
gint32 managed_id;
guint32 small_id;
......
......@@ -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*
......
......@@ -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;
......
......@@ -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)
......
......@@ -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)
{
}
......
......@@ -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)
......
......@@ -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);
......
......@@ -18,6 +18,77 @@ public static int Main(string[] args)
return 0;
}
[JSExport]
public static async Task TestCanStartThread()
{
var tcs = new TaskCompletionSource<int>();
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<JSType.Function>] Action cb, int timeoutMs);
[JSImport("globalThis.fetch")]
private static partial Task<JSObject> 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<string> FetchHelperResponseText(JSObject response);
[JSExport]
public static async Task<string> 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<int> RunBackgroundThreadCompute()
{
......@@ -41,10 +112,27 @@ public static async Task<int> RunBackgroundLongRunningTaskCompute()
return await t;
}
[JSExport]
public static async Task<int> 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;
......
......@@ -6,6 +6,8 @@
<ItemGroup>
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="fetchhelper.js" />
<WasmExtraFilesToDeploy Include="blurst.txt" />
</ItemGroup>
<!-- set the condition to false and you will get a CA1416 error about the call to Thread.Start from a browser-wasm project -->
......
It was the best of times, it was the blurst of times.
function delay(timeoutMs) {
return new Promise(resolve => setTimeout(resolve, timeoutMs));
}
export async function responseText(response) /* Promise<string> */ {
console.log("artificially waiting for response for 25 seconds");
await delay(25000);
console.log("artificial waiting done");
return await response.text();
}
......@@ -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);
......
......@@ -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 = (<any>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));
}
......
......@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册