From 8243627d0160e17171825519500a29a68900cf2e Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 6 Jul 2021 17:07:36 -0700 Subject: [PATCH 1/3] Add NoThrowAwaitable for MainThreadAwaitable Fixes #877 --- .../JoinableTaskFactory.cs | 176 +++++++++++++++++- .../TplExtensions.cs | 12 +- .../net472/PublicAPI.Unshipped.txt | 8 + .../net6.0-windows/PublicAPI.Unshipped.txt | 8 + .../net6.0/PublicAPI.Unshipped.txt | 8 + .../netstandard2.0/PublicAPI.Unshipped.txt | 8 + 6 files changed, 211 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs index c24f07553..a1c468c87 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs @@ -709,6 +709,19 @@ internal MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, this.alwaysYield = alwaysYield; } + internal NoThrowMainThreadAwaitable NoThrowAwaitable + { + get + { + if (this.jobFactory is null) + { + return default; + } + + return new NoThrowMainThreadAwaitable(this.jobFactory, this.job, this.cancellationToken, this.alwaysYield); + } + } + /// /// Gets the awaiter. /// @@ -723,10 +736,146 @@ public MainThreadAwaiter GetAwaiter() } } + /// + /// An awaitable struct that facilitates an asynchronous transition to the Main thread. + /// + public readonly struct NoThrowMainThreadAwaitable + { + private readonly JoinableTaskFactory? jobFactory; + + private readonly JoinableTask? job; + + private readonly CancellationToken cancellationToken; + + private readonly bool alwaysYield; + + /// + /// Initializes a new instance of the struct. + /// + internal NoThrowMainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, CancellationToken cancellationToken, bool alwaysYield = false) + { + Requires.NotNull(jobFactory, nameof(jobFactory)); + + this.jobFactory = jobFactory; + this.job = job; + this.cancellationToken = cancellationToken; + this.alwaysYield = alwaysYield; + } + + /// + /// Gets the awaiter. + /// + public NoThrowMainThreadAwaiter GetAwaiter() + { + if (this.jobFactory is null) + { + return default; + } + + return new NoThrowMainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.cancellationToken); + } + } + /// /// An awaiter struct that facilitates an asynchronous transition to the Main thread. /// public readonly struct MainThreadAwaiter : ICriticalNotifyCompletion + { + private readonly MainThreadAwaiterImpl impl; + + /// + /// Initializes a new instance of the struct. + /// + internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) + { + this.impl = new MainThreadAwaiterImpl(jobFactory, job, alwaysYield, throwOnCancellation: true, cancellationToken); + } + + /// + /// Gets a value indicating whether the caller is already on the Main thread. + /// + public bool IsCompleted => this.impl.IsCompleted; + + /// + /// Schedules a continuation for execution on the Main thread + /// without capturing the ExecutionContext. + /// + /// The action to invoke when the operation completes. + public void UnsafeOnCompleted(Action continuation) + { + this.impl.UnsafeOnCompleted(continuation); + } + + /// + /// Schedules a continuation for execution on the Main thread. + /// + /// The action to invoke when the operation completes. + public void OnCompleted(Action continuation) + { + this.impl.OnCompleted(continuation); + } + + /// + /// Called on the Main thread to prepare it to execute the continuation. + /// + public void GetResult() + { + this.impl.GetResult(); + } + } + + /// + /// An awaiter struct that facilitates an asynchronous transition to the Main thread. + /// + public readonly struct NoThrowMainThreadAwaiter : ICriticalNotifyCompletion + { + private readonly MainThreadAwaiterImpl impl; + + /// + /// Initializes a new instance of the struct. + /// + internal NoThrowMainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) + { + this.impl = new MainThreadAwaiterImpl(jobFactory, job, alwaysYield, throwOnCancellation: false, cancellationToken); + } + + /// + /// Gets a value indicating whether the caller is already on the Main thread. + /// + public bool IsCompleted => this.impl.IsCompleted; + + /// + /// Schedules a continuation for execution on the Main thread + /// without capturing the ExecutionContext. + /// + /// The action to invoke when the operation completes. + public void UnsafeOnCompleted(Action continuation) + { + this.impl.UnsafeOnCompleted(continuation); + } + + /// + /// Schedules a continuation for execution on the Main thread. + /// + /// The action to invoke when the operation completes. + public void OnCompleted(Action continuation) + { + this.impl.OnCompleted(continuation); + } + + /// + /// Called on the Main thread to prepare it to execute the continuation. + /// + public bool GetResult() + { + return this.impl.GetResult(); + } + } + + /// + /// An awaiter struct that facilitates an asynchronous transition to the Main thread. + /// + private readonly struct MainThreadAwaiterImpl : ICriticalNotifyCompletion { private static readonly Action SafeCancellationAction = state => ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); @@ -738,6 +887,8 @@ public MainThreadAwaiter GetAwaiter() private readonly bool alwaysYield; + private readonly bool throwOnCancellation; + private readonly JoinableTask? job; private readonly bool synchronousCancellation; @@ -760,15 +911,16 @@ public MainThreadAwaiter GetAwaiter() private readonly StrongBox? cancellationRegistrationPtr; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) + internal MainThreadAwaiterImpl(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) { this.jobFactory = jobFactory; this.job = job; this.cancellationToken = cancellationToken; this.synchronousCancellation = cancellationToken.IsCancellationRequested && !alwaysYield; this.alwaysYield = alwaysYield; + this.throwOnCancellation = throwOnCancellation; // Don't allocate the pointer if the cancellation token can't be canceled (or already is): this.cancellationRegistrationPtr = cancellationToken.CanBeCanceled && !this.synchronousCancellation @@ -817,7 +969,7 @@ public void OnCompleted(Action continuation) /// /// Called on the Main thread to prepare it to execute the continuation. /// - public void GetResult() + public bool GetResult() { Assumes.True(this.jobFactory is object); if (!(this.jobFactory.Context.IsOnMainThread || this.jobFactory.Context.UnderlyingSynchronizationContext is null || this.cancellationToken.IsCancellationRequested)) @@ -867,11 +1019,19 @@ public void GetResult() SynchronizationContext? syncContext = this.job is object ? this.job.ApplicableJobSyncContext : this.jobFactory.ApplicableJobSyncContext; syncContext.Apply(); - // Cancel if requested, even if we arrived on the main thread. - // Unlike most async methods where throwing OperationCanceledException after completing the work may not be a good idea, - // SwitchToMainThreadAsync is a scheduler method, and always precedes some work by the caller that almost certainly should - // not be carried out if cancellation was requested. - this.cancellationToken.ThrowIfCancellationRequested(); + if (this.throwOnCancellation) + { + // Cancel if requested, even if we arrived on the main thread. + // Unlike most async methods where throwing OperationCanceledException after completing the work may not be a good idea, + // SwitchToMainThreadAsync is a scheduler method, and always precedes some work by the caller that almost certainly should + // not be carried out if cancellation was requested. + this.cancellationToken.ThrowIfCancellationRequested(); + return true; + } + else + { + return !this.cancellationToken.IsCancellationRequested; + } } /// diff --git a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs index b62198fc3..40de50e49 100644 --- a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Security; using System.Threading; using System.Threading.Tasks; @@ -241,6 +240,17 @@ public static NoThrowTaskAwaitable NoThrowAwaitable(this Task task, bool capture return new NoThrowTaskAwaitable(task, captureContext); } + /// + /// Returns an awaitable for the specified + /// operation that will return a Boolean value indicating cancellation rather than throwing an exception. + /// + /// The task whose completion should signal the completion of the returned awaitable. + /// An awaitable. + public static JoinableTaskFactory.NoThrowMainThreadAwaitable NoThrowAwaitable(this JoinableTaskFactory.MainThreadAwaitable awaitable) + { + return awaitable.NoThrowAwaitable; + } + /// /// Consumes a task and doesn't do anything with it. Useful for fire-and-forget calls to async methods within async methods. /// diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt index eea3008f0..cb749cf12 100644 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt @@ -1,3 +1,11 @@ +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void +static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt index eea3008f0..cb749cf12 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt @@ -1,3 +1,11 @@ +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void +static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt index eea3008f0..cb749cf12 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt @@ -1,3 +1,11 @@ +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void +static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt index eea3008f0..cb749cf12 100644 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,11 @@ +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void +Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void +static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! From 3a3034a7890d2c74c6ecc168358a5d06591ba91a Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 5 Jun 2023 11:22:35 -0500 Subject: [PATCH 2/3] Simplify implementation of no-throw switch to main thread --- .../JoinableTaskFactory.cs | 170 +++--------------- .../TplExtensions.cs | 12 +- .../net472/PublicAPI.Unshipped.txt | 9 +- .../net6.0-windows/PublicAPI.Unshipped.txt | 9 +- .../net6.0/PublicAPI.Unshipped.txt | 9 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 9 +- 6 files changed, 26 insertions(+), 192 deletions(-) diff --git a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs index a1c468c87..05d25f268 100644 --- a/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs +++ b/src/Microsoft.VisualStudio.Threading/JoinableTaskFactory.cs @@ -696,10 +696,20 @@ public readonly struct MainThreadAwaitable private readonly bool alwaysYield; + private readonly bool throwOnCancellation; + /// /// Initializes a new instance of the struct. /// internal MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, CancellationToken cancellationToken, bool alwaysYield = false) + : this(jobFactory, job, alwaysYield, throwOnCancellation: true, cancellationToken) + { + } + + /// + /// Initializes a new instance of the struct. + /// + private MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) { Requires.NotNull(jobFactory, nameof(jobFactory)); @@ -707,72 +717,35 @@ internal MainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, this.job = job; this.cancellationToken = cancellationToken; this.alwaysYield = alwaysYield; - } - - internal NoThrowMainThreadAwaitable NoThrowAwaitable - { - get - { - if (this.jobFactory is null) - { - return default; - } - - return new NoThrowMainThreadAwaitable(this.jobFactory, this.job, this.cancellationToken, this.alwaysYield); - } + this.throwOnCancellation = throwOnCancellation; } /// - /// Gets the awaiter. + /// Returns an awaitable for the specified + /// operation that will not throw an exception if cancellation is requested. /// - public MainThreadAwaiter GetAwaiter() + /// An awaitable. + public MainThreadAwaitable NoThrowAwaitable() { if (this.jobFactory is null) { return default; } - return new MainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.cancellationToken); - } - } - - /// - /// An awaitable struct that facilitates an asynchronous transition to the Main thread. - /// - public readonly struct NoThrowMainThreadAwaitable - { - private readonly JoinableTaskFactory? jobFactory; - - private readonly JoinableTask? job; - - private readonly CancellationToken cancellationToken; - - private readonly bool alwaysYield; - - /// - /// Initializes a new instance of the struct. - /// - internal NoThrowMainThreadAwaitable(JoinableTaskFactory jobFactory, JoinableTask? job, CancellationToken cancellationToken, bool alwaysYield = false) - { - Requires.NotNull(jobFactory, nameof(jobFactory)); - - this.jobFactory = jobFactory; - this.job = job; - this.cancellationToken = cancellationToken; - this.alwaysYield = alwaysYield; + return new MainThreadAwaitable(this.jobFactory, this.job, this.alwaysYield, throwOnCancellation: false, this.cancellationToken); } /// /// Gets the awaiter. /// - public NoThrowMainThreadAwaiter GetAwaiter() + public MainThreadAwaiter GetAwaiter() { if (this.jobFactory is null) { return default; } - return new NoThrowMainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.cancellationToken); + return new MainThreadAwaiter(this.jobFactory, this.job, this.alwaysYield, this.throwOnCancellation, this.cancellationToken); } } @@ -780,102 +753,6 @@ public NoThrowMainThreadAwaiter GetAwaiter() /// An awaiter struct that facilitates an asynchronous transition to the Main thread. /// public readonly struct MainThreadAwaiter : ICriticalNotifyCompletion - { - private readonly MainThreadAwaiterImpl impl; - - /// - /// Initializes a new instance of the struct. - /// - internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) - { - this.impl = new MainThreadAwaiterImpl(jobFactory, job, alwaysYield, throwOnCancellation: true, cancellationToken); - } - - /// - /// Gets a value indicating whether the caller is already on the Main thread. - /// - public bool IsCompleted => this.impl.IsCompleted; - - /// - /// Schedules a continuation for execution on the Main thread - /// without capturing the ExecutionContext. - /// - /// The action to invoke when the operation completes. - public void UnsafeOnCompleted(Action continuation) - { - this.impl.UnsafeOnCompleted(continuation); - } - - /// - /// Schedules a continuation for execution on the Main thread. - /// - /// The action to invoke when the operation completes. - public void OnCompleted(Action continuation) - { - this.impl.OnCompleted(continuation); - } - - /// - /// Called on the Main thread to prepare it to execute the continuation. - /// - public void GetResult() - { - this.impl.GetResult(); - } - } - - /// - /// An awaiter struct that facilitates an asynchronous transition to the Main thread. - /// - public readonly struct NoThrowMainThreadAwaiter : ICriticalNotifyCompletion - { - private readonly MainThreadAwaiterImpl impl; - - /// - /// Initializes a new instance of the struct. - /// - internal NoThrowMainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, CancellationToken cancellationToken) - { - this.impl = new MainThreadAwaiterImpl(jobFactory, job, alwaysYield, throwOnCancellation: false, cancellationToken); - } - - /// - /// Gets a value indicating whether the caller is already on the Main thread. - /// - public bool IsCompleted => this.impl.IsCompleted; - - /// - /// Schedules a continuation for execution on the Main thread - /// without capturing the ExecutionContext. - /// - /// The action to invoke when the operation completes. - public void UnsafeOnCompleted(Action continuation) - { - this.impl.UnsafeOnCompleted(continuation); - } - - /// - /// Schedules a continuation for execution on the Main thread. - /// - /// The action to invoke when the operation completes. - public void OnCompleted(Action continuation) - { - this.impl.OnCompleted(continuation); - } - - /// - /// Called on the Main thread to prepare it to execute the continuation. - /// - public bool GetResult() - { - return this.impl.GetResult(); - } - } - - /// - /// An awaiter struct that facilitates an asynchronous transition to the Main thread. - /// - private readonly struct MainThreadAwaiterImpl : ICriticalNotifyCompletion { private static readonly Action SafeCancellationAction = state => ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, state); @@ -911,9 +788,9 @@ public bool GetResult() private readonly StrongBox? cancellationRegistrationPtr; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - internal MainThreadAwaiterImpl(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) + internal MainThreadAwaiter(JoinableTaskFactory jobFactory, JoinableTask? job, bool alwaysYield, bool throwOnCancellation, CancellationToken cancellationToken) { this.jobFactory = jobFactory; this.job = job; @@ -969,7 +846,7 @@ public void OnCompleted(Action continuation) /// /// Called on the Main thread to prepare it to execute the continuation. /// - public bool GetResult() + public void GetResult() { Assumes.True(this.jobFactory is object); if (!(this.jobFactory.Context.IsOnMainThread || this.jobFactory.Context.UnderlyingSynchronizationContext is null || this.cancellationToken.IsCancellationRequested)) @@ -1026,11 +903,6 @@ public bool GetResult() // SwitchToMainThreadAsync is a scheduler method, and always precedes some work by the caller that almost certainly should // not be carried out if cancellation was requested. this.cancellationToken.ThrowIfCancellationRequested(); - return true; - } - else - { - return !this.cancellationToken.IsCancellationRequested; } } diff --git a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs index 40de50e49..b62198fc3 100644 --- a/src/Microsoft.VisualStudio.Threading/TplExtensions.cs +++ b/src/Microsoft.VisualStudio.Threading/TplExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Security; using System.Threading; using System.Threading.Tasks; @@ -240,17 +241,6 @@ public static NoThrowTaskAwaitable NoThrowAwaitable(this Task task, bool capture return new NoThrowTaskAwaitable(task, captureContext); } - /// - /// Returns an awaitable for the specified - /// operation that will return a Boolean value indicating cancellation rather than throwing an exception. - /// - /// The task whose completion should signal the completion of the returned awaitable. - /// An awaitable. - public static JoinableTaskFactory.NoThrowMainThreadAwaitable NoThrowAwaitable(this JoinableTaskFactory.MainThreadAwaitable awaitable) - { - return awaitable.NoThrowAwaitable; - } - /// /// Consumes a task and doesn't do anything with it. Useful for fire-and-forget calls to async methods within async methods. /// diff --git a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt index cb749cf12..9cf850548 100644 --- a/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net472/PublicAPI.Unshipped.txt @@ -1,11 +1,4 @@ -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.NoThrowAwaitable() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt index cb749cf12..9cf850548 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0-windows/PublicAPI.Unshipped.txt @@ -1,11 +1,4 @@ -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.NoThrowAwaitable() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt index cb749cf12..9cf850548 100644 --- a/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/net6.0/PublicAPI.Unshipped.txt @@ -1,11 +1,4 @@ -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.NoThrowAwaitable() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! diff --git a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt index cb749cf12..9cf850548 100644 --- a/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualStudio.Threading/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,11 +1,4 @@ -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable.GetAwaiter() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.GetResult() -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.IsCompleted.get -> bool -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.OnCompleted(System.Action! continuation) -> void -Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaiter.UnsafeOnCompleted(System.Action! continuation) -> void -static Microsoft.VisualStudio.Threading.TplExtensions.NoThrowAwaitable(this Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable awaitable) -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.NoThrowMainThreadAwaitable +Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable.NoThrowAwaitable() -> Microsoft.VisualStudio.Threading.JoinableTaskFactory.MainThreadAwaitable virtual Microsoft.VisualStudio.Threading.AsyncReaderWriterResourceLock.GetTaskSchedulerToPrepareResourcesForConcurrentAccess(TResource! resource) -> System.Threading.Tasks.TaskScheduler! Microsoft.VisualStudio.Threading.JoinableTaskContext.Capture() -> string? Microsoft.VisualStudio.Threading.JoinableTaskFactory.RunAsync(System.Func! asyncMethod, string? parentToken, Microsoft.VisualStudio.Threading.JoinableTaskCreationOptions creationOptions) -> Microsoft.VisualStudio.Threading.JoinableTask! From a094f9b633f701cda77840aa6403f8d62b2b62e9 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 5 Jun 2023 11:42:04 -0500 Subject: [PATCH 3/3] Add tests for no-throw switch to main thread --- .../JoinableTaskTests.cs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs index 7b615f24d..fd0f54f20 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/JoinableTaskTests.cs @@ -306,6 +306,21 @@ public void SwitchToMainThreadCancellable() Assert.True(task.Wait(TestTimeout * 3), "Test timed out."); } + [Fact] + public void SwitchToMainThreadNoThrowCancellable() + { + var task = Task.Run(async delegate + { + var cts = new CancellationTokenSource(AsyncDelay); + await this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable(); + + Assert.Null(SynchronizationContext.Current); + Assert.NotEqual(this.originalThreadManagedId, Environment.CurrentManagedThreadId); + }); + + Assert.True(task.Wait(TestTimeout * 3), "Test timed out."); + } + [Fact] public void SwitchToMainThreadCancellableWithinRun() { @@ -364,6 +379,26 @@ public void SwitchToMainThreadAsync_Await_Canceled_CapturesExecutionContext() task.Wait(this.TimeoutToken); } + [Fact] + public void SwitchToMainThreadAsync_NoThrowAwait_Canceled_CapturesExecutionContext() + { + var factory = (DerivedJoinableTaskFactory)this.asyncPump; + var cts = new CancellationTokenSource(); + var transitionRequested = new ManualResetEventSlim(); + factory.TransitioningToMainThreadCallback = jt => transitionRequested.Set(); + var task = Task.Run(async delegate + { + var asyncLocal = new System.Threading.AsyncLocal(); + asyncLocal.Value = "expected"; + await this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable(); + Assert.NotEqual(this.originalThreadManagedId, Environment.CurrentManagedThreadId); + Assert.Equal("expected", asyncLocal.Value); + }); + transitionRequested.Wait(); + cts.Cancel(); + task.Wait(this.TimeoutToken); + } + [Fact] public void SwitchToMainThreadAsync_UnsafeOnCompleted_DoesNotCaptureExecutionContext() { @@ -489,6 +524,21 @@ public void SwitchToMainThread_PrecanceledOnMainThread() }); } + [Fact] + public void SwitchToMainThread_PrecanceledOnMainThread_NoThrow() + { + this.SimulateUIThread(async delegate + { + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(new CancellationToken(true)).NoThrowAwaitable().GetAwaiter(); + Assert.True(awaiter.IsCompleted); + awaiter.GetResult(); + + // Verify that the SynchronizationContext remains such that we stay on the main thread after yielding. + await Task.Yield(); + Assert.True(this.context.IsOnMainThread); + }); + } + [Fact] public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired() { @@ -514,6 +564,31 @@ public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired() }); } + [Fact] + public void SwitchToMainThread_PrecanceledOnMainThread_StillYieldsWhenRequired_NoThrow() + { + this.SimulateUIThread(async delegate + { + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(alwaysYield: true, new CancellationToken(true)).NoThrowAwaitable().GetAwaiter(); + Assert.False(awaiter.IsCompleted); + var testResult = new TaskCompletionSource(); + awaiter.OnCompleted(delegate + { + try + { + awaiter.GetResult(); + Assert.Equal(this.context.IsOnMainThread, SynchronizationContext.Current is object); + testResult.SetResult(null); + } + catch (Exception ex) + { + testResult.SetException(ex); + } + }); + await testResult.Task.WithCancellation(this.TimeoutToken); + }); + } + [Fact] public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMainThread() { @@ -531,6 +606,22 @@ public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMain }); } + [Fact] + public void SwitchToMainThreadAsync_CompletesSynchronouslyWhenPreCanceledOffMainThread_NoThrow() + { + this.SimulateUIThread(delegate + { + return Task.Run(delegate + { + var precanceled = new CancellationToken(canceled: true); + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(precanceled).NoThrowAwaitable().GetAwaiter(); + Assert.True(awaiter.IsCompleted); + awaiter.GetResult(); + Assert.Null(SynchronizationContext.Current); + }); + }); + } + [Fact] public void SwitchToMainThreadAsync_CanceledToBackgroundThreadWithSyncContext() { @@ -604,6 +695,36 @@ public void SwitchToMainThreadAsync_ThrowsOnCancellationAfterReachingMainThread( }); } + [Fact] + public void SwitchToMainThreadAsync_NoThrowOnCancellationAfterReachingMainThread() + { + this.SimulateUIThread(delegate + { + return Task.Run(async delegate + { + var cts = new CancellationTokenSource(); + JoinableTaskFactory.MainThreadAwaiter awaiter = this.asyncPump.SwitchToMainThreadAsync(cts.Token).NoThrowAwaitable().GetAwaiter(); + Assert.False(awaiter.IsCompleted); + var testResult = new TaskCompletionSource(); + awaiter.OnCompleted(delegate + { + try + { + Assert.True(this.context.IsOnMainThread); + cts.Cancel(); + awaiter.GetResult(); + testResult.SetResult(null); + } + catch (Exception ex) + { + testResult.SetException(ex); + } + }); + await testResult.Task.WithCancellation(this.TimeoutToken); + }); + }); + } + /// /// Verify that if the was initialized /// without a whose