Skip to content
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

[wasm][mt] Guard more places for blocking wait on JS interop threads #98508

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ private void DisposeCore()

private void AcquireCore()
{
#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif
Interop.Sys.LowLevelMonitor_Acquire(_nativeMonitor);
}

Expand All @@ -41,6 +44,9 @@ private void ReleaseCore()

private void WaitCore()
{
#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif
Interop.Sys.LowLevelMonitor_Wait(_nativeMonitor);
}

Expand All @@ -54,6 +60,10 @@ private bool WaitCore(int timeoutMilliseconds)
return true;
}

#if FEATURE_WASM_MANAGED_THREADS
Thread.AssureBlockingPossible();
#endif

return Interop.Sys.LowLevelMonitor_TimedWait(_nativeMonitor, timeoutMilliseconds);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,15 +366,18 @@ 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);

using var timer = new Timer(_ =>
await ForceBlockingWaitAsync(async () =>
{
Assert.NotEqual(1, Environment.CurrentManagedThreadId);
Assert.True(Thread.CurrentThread.IsThreadPoolThread);
hit = true;
tcs.SetResult();
}, null, 100, Timeout.Infinite);

await tcs.Task;
using var timer = new Timer(_ =>
{
Assert.NotEqual(1, Environment.CurrentManagedThreadId);
Assert.True(Thread.CurrentThread.IsThreadPoolThread);
hit = true;
tcs.SetResult();
}, null, 100, Timeout.Infinite);

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);
Expand Down Expand Up @@ -411,17 +414,36 @@ await executor.Execute(async () =>
}, cts.Token);
}

static async Task ForceBlockingWaitAsync(Func<Task> action)
{
var flag = Thread.ThrowOnBlockingWaitOnJSInteropThread;
try
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = false;

await action();
}
finally
{
Thread.ThrowOnBlockingWaitOnJSInteropThread = flag;
}
}


[Theory, MemberData(nameof(GetTargetThreads))]
public async Task ManagedDelay_ContinueWith(Executor executor)
{
var hit = false;
using var cts = CreateTestCaseTimeoutSource();
await executor.Execute(async () =>
{
await Task.Delay(10, cts.Token).ContinueWith(_ =>
await ForceBlockingWaitAsync(async () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree that making Task.Delay forbidden operation on UI thread is a way forward.
I think we have to find a way that it never calls blocking .Wait

Here you are just hiding the problem from the unit test, rigth ?

Copy link
Member

@pavelsavara pavelsavara Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the same for Timer, there is no theoretical reason why this needs to block.
Except maybe for creating new child thread. If that's the reason, we should add ForceBlockingWaitAsync into new Thread() code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree that making Task.Delay forbidden operation on UI thread is a way forward. I think we have to find a way that it never calls blocking .Wait

Here you are just hiding the problem from the unit test, rigth ?

Yes, here I am forcing it to not break the current ManagedDelay_ContinueWith test.

The Task.Delay eventually ends calling System.Threading.LowLevelMonitor.AcquireCore(), which calls to native pthread_mutex_lock - that is a blocking call.

The Timer calls LowLevelMonitor.AcquireCore as well.

Details:

  info: System.PlatformNotSupportedException : Blocking wait is not supported on the JS interop threads.
  info:    at System.Threading.Thread.AssureBlockingPossible() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs:line 683
  info:    at System.Threading.LowLevelMonitor.AcquireCore() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelMonitor.Unix.cs:line 35
  info:    at System.Threading.LowLevelMonitor.Acquire() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelMonitor.cs:line 78
  info:    at System.Threading.WaitSubsystem.ThreadWaitInfo.TrySignalToSatisfyWait(WaitedListNode , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.ThreadWaitInfo.Unix.cs:line 448
  info:    at System.Threading.WaitSubsystem.WaitableObject.SignalAutoResetEvent() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs:line 709
  info:    at System.Threading.WaitSubsystem.WaitableObject.SignalEvent(LockHolder& ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs:line 658
  info:    at System.Threading.WaitSubsystem.SetEvent(WaitableObject ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs:line 254
  info:    at System.Threading.WaitSubsystem.SetEvent(IntPtr ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs:line 244
  info:    at System.Threading.EventWaitHandle.Set() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.Unix.cs:line 48
  info:    at System.Threading.TimerQueue.SetTimerPortable(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs:line 66
  info:    at System.Threading.TimerQueue.SetTimer(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs:line 16
  info:    at System.Threading.TimerQueue.EnsureTimerFiresBy(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 138
  info:    at System.Threading.TimerQueue.UpdateTimer(TimerQueueTimer , UInt32 , UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 368
  info:    at System.Threading.TimerQueueTimer.Change(UInt32 , UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 561
  info:    at System.Threading.TimerQueueTimer..ctor(TimerCallback , Object , UInt32 , UInt32 , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 516
  info:    at System.Threading.Tasks.Task.DelayPromise..ctor(UInt32 , TimeProvider ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5704
  info:    at System.Threading.Tasks.Task.DelayPromiseWithCancellation..ctor(UInt32 , TimeProvider , CancellationToken ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5749
  info:    at System.Threading.Tasks.Task.Delay(UInt32 , TimeProvider , CancellationToken ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5668
  info:    at System.Threading.Tasks.Task.Delay(Int32 millisecondsDelay, CancellationToken cancellationToken) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5664
  info:    at System.Runtime.InteropServices.JavaScript.Tests.WebWorkerTest.<>c__DisplayClass15_0.<<ManagedDelay_ContinueWith>b__0>d.MoveNext() in /Users/rodo/git/threads-runtime/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs:line 442
  info: [FAIL] System.Runtime.InteropServices.JavaScript.Tests.WebWorkerTest.ThreadingTimer(executor: Main)
  info: System.PlatformNotSupportedException : Blocking wait is not supported on the JS interop threads.
  info:    at System.Threading.Thread.AssureBlockingPossible() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs:line 683
  info:    at System.Threading.LowLevelMonitor.AcquireCore() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelMonitor.Unix.cs:line 35
  info:    at System.Threading.LowLevelMonitor.Acquire() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelMonitor.cs:line 78
  info:    at System.Threading.WaitSubsystem.ThreadWaitInfo.TrySignalToSatisfyWait(WaitedListNode , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.ThreadWaitInfo.Unix.cs:line 448
  info:    at System.Threading.WaitSubsystem.WaitableObject.SignalAutoResetEvent() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs:line 709
  info:    at System.Threading.WaitSubsystem.WaitableObject.SignalEvent(LockHolder& ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs:line 658
  info:    at System.Threading.WaitSubsystem.SetEvent(WaitableObject ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs:line 254
  info:    at System.Threading.WaitSubsystem.SetEvent(IntPtr ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs:line 244
  info:    at System.Threading.EventWaitHandle.Set() in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.Unix.cs:line 48
  info:    at System.Threading.TimerQueue.SetTimerPortable(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Portable.cs:line 66
  info:    at System.Threading.TimerQueue.SetTimer(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/TimerQueue.Unix.cs:line 16
  info:    at System.Threading.TimerQueue.EnsureTimerFiresBy(UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 138
  info:    at System.Threading.TimerQueue.UpdateTimer(TimerQueueTimer , UInt32 , UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 368
  info:    at System.Threading.TimerQueueTimer.Change(UInt32 , UInt32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 561
  info:    at System.Threading.TimerQueueTimer..ctor(TimerCallback , Object , UInt32 , UInt32 , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 516
  info:    at System.Threading.Timer.TimerSetup(TimerCallback , Object , UInt32 , UInt32 , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 905
  info:    at System.Threading.Timer..ctor(TimerCallback , Object , Int32 , Int32 , Boolean ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 845
  info:    at System.Threading.Timer..ctor(TimerCallback , Object , Int32 , Int32 ) in /Users/rodo/git/threads-runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs:line 832
  info:    at System.Runtime.InteropServices.JavaScript.Tests.WebWorkerTest.<>c__DisplayClass11_0.<<ThreadingTimer>b__0>d.MoveNext() in /Users/rodo/git/threads-runtime/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.cs:line 371

{
hit = true;
}, TaskContinuationOptions.ExecuteSynchronously);
await Task.Delay(10, cts.Token).ContinueWith(_ =>
{
hit = true;
}, TaskContinuationOptions.ExecuteSynchronously);
});
}, cts.Token);
Assert.True(hit);
}
Expand All @@ -432,7 +454,10 @@ public async Task ManagedDelay_ConfigureAwait_True(Executor executor)
using var cts = CreateTestCaseTimeoutSource();
await executor.Execute(async () =>
{
await Task.Delay(10, cts.Token).ConfigureAwait(true);
await ForceBlockingWaitAsync(async () =>
{
await Task.Delay(10, cts.Token).ConfigureAwait(true);
});

executor.AssertAwaitCapturedContext();
}, cts.Token);
Expand All @@ -441,23 +466,32 @@ await executor.Execute(async () =>
[Theory, MemberData(nameof(GetTargetThreadsAndBlockingCalls))]
public async Task WaitAssertsOnJSInteropThreads(Executor executor, NamedCall method)
{
using var cts = CreateTestCaseTimeoutSource();
await executor.Execute(Task () =>
CancellationTokenSource? cts = null;
radekdoulik marked this conversation as resolved.
Show resolved Hide resolved
try
{
Exception? exception = null;
try
{
method.Call(cts.Token);
}
catch (Exception ex)
Thread.ForceBlockingWait((_) => cts = CreateTestCaseTimeoutSource(), null);
await executor.Execute(Task () =>
{
exception = ex;
}
Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
executor.AssertBlockingWait(exception);
Exception? exception = null;
try
{
method.Call(cts.Token);
}
catch (Exception ex)
{
exception = ex;
}

return Task.CompletedTask;
}, cts.Token);
Console.WriteLine("WaitAssertsOnJSInteropThreads: ExecuterType: " + executor.Type + " ManagedThreadId: " + Environment.CurrentManagedThreadId + " NativeThreadId: " + WebWorkerTestHelper.NativeThreadId);
executor.AssertBlockingWait(exception);

return Task.CompletedTask;
}, cts.Token);
}
finally
{
cts?.Dispose();
}
}

[Theory, MemberData(nameof(GetTargetThreads))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,48 @@ public class NamedCall
override public string ToString() => Name;
}

static void LocalCtsIgnoringCall(Action<CancellationToken> action)
{
var cts = new CancellationTokenSource(8);
try {
action(cts.Token);
} catch (OperationCanceledException exception) {
if (exception.CancellationToken != cts.Token)
{
throw;
}
/* ignore the local one */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkhamoyan Radek improved it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will update in my PR.

}
}

public static IEnumerable<NamedCall> BlockingCalls = new List<NamedCall>
{
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 */ }
LocalCtsIgnoringCall(mr.Wait);
}},
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 */ }
LocalCtsIgnoringCall(sem.Wait);
}},
new NamedCall { Name = "CancellationTokenSource.ctor", Call = delegate (CancellationToken ct) {
using var cts = new CancellationTokenSource(8);
}},
new NamedCall { 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((_) => mr.Wait(), null);
mutex.WaitOne();
}},
};

Expand Down
Loading