// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.VisualStudio.Threading
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JoinableTaskSynchronizationContext = Microsoft.VisualStudio.Threading.JoinableTask.JoinableTaskSynchronizationContext;
///
/// A factory for starting asynchronous tasks that can mitigate deadlocks
/// when the tasks require the Main thread of an application and the Main
/// thread may itself be blocking on the completion of a task.
///
///
/// For more complete comments please see the .
///
public partial class JoinableTaskFactory
{
///
/// The that owns this instance.
///
private readonly JoinableTaskContext owner;
private readonly SynchronizationContext mainThreadJobSyncContext;
///
/// The collection to add all created tasks to. May be null.
///
private readonly JoinableTaskCollection? jobCollection;
///
/// Backing field for the property.
///
private TimeSpan hangDetectionTimeout = TimeSpan.FromSeconds(6);
///
/// Initializes a new instance of the class.
///
/// The context for the tasks created by this factory.
public JoinableTaskFactory(JoinableTaskContext owner)
: this(owner, null)
{
}
///
/// Initializes a new instance of the class
/// that adds all generated jobs to the specified collection.
///
/// The collection that all tasks created by this factory will belong to till they complete.
public JoinableTaskFactory(JoinableTaskCollection collection)
: this(Requires.NotNull(collection, "collection").Context, collection)
{
}
///
/// Initializes a new instance of the class.
///
/// The context for the tasks created by this factory.
/// The collection that all tasks created by this factory will belong to till they complete. May be null.
internal JoinableTaskFactory(JoinableTaskContext owner, JoinableTaskCollection? collection)
{
Requires.NotNull(owner, nameof(owner));
Assumes.True(collection is null || collection.Context == owner);
this.owner = owner;
this.jobCollection = collection;
this.mainThreadJobSyncContext = new JoinableTaskSynchronizationContext(this);
}
///
/// Gets the joinable task context to which this factory belongs.
///
public JoinableTaskContext Context
{
get { return this.owner; }
}
///
/// Gets the synchronization context to apply before executing work associated with this factory.
///
internal SynchronizationContext? ApplicableJobSyncContext
{
get { return this.Context.IsOnMainThread ? this.mainThreadJobSyncContext : null; }
}
///
/// Gets the collection to which created tasks belong until they complete. May be null.
///
internal JoinableTaskCollection? Collection
{
get { return this.jobCollection; }
}
///
/// Gets or sets the timeout after which no activity while synchronously blocking
/// suggests a hang has occurred.
///
protected TimeSpan HangDetectionTimeout
{
get
{
return this.hangDetectionTimeout;
}
set
{
Requires.Range(value > TimeSpan.Zero, "value");
this.hangDetectionTimeout = value;
}
}
///
/// Gets the underlying that controls the main thread in the host.
///
protected SynchronizationContext? UnderlyingSynchronizationContext
{
get { return this.Context.UnderlyingSynchronizationContext; }
}
///
/// Gets an awaitable whose continuations execute on the synchronization context that this instance was initialized with,
/// in such a way as to mitigate both deadlocks and reentrancy.
///
///
/// A token whose cancellation will immediately schedule the continuation
/// on a threadpool thread and will cause the continuation to throw ,
/// even if the caller is already on the main thread.
///
/// An awaitable.
///
/// Thrown back at the awaiting caller if is canceled,
/// even if the caller is already on the main thread.
///
///
///
///
/// private async Task SomeOperationAsync() {
/// // on the caller's thread.
/// await DoAsync();
///
/// // Now switch to a threadpool thread explicitly.
/// await TaskScheduler.Default;
///
/// // Now switch to the Main thread to talk to some STA object.
/// await this.JobContext.SwitchToMainThreadAsync();
/// STAService.DoSomething();
/// }
///
///
///
public MainThreadAwaitable SwitchToMainThreadAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken);
}
///
/// Gets an awaitable whose continuations execute on the synchronization context that this instance was initialized with,
/// in such a way as to mitigate both deadlocks and reentrancy.
///
/// A value indicating whether the caller should yield even if
/// already executing on the main thread.
///
/// A token whose cancellation will immediately schedule the continuation
/// on a threadpool thread and will cause the continuation to throw ,
/// even if the caller is already on the main thread.
///
/// An awaitable.
///
/// Thrown back at the awaiting caller if is canceled,
/// even if the caller is already on the main thread.
///
///
///
///
/// private async Task SomeOperationAsync()
/// {
/// // This first part can be on the caller's thread, whatever that is.
/// DoSomething();
///
/// // Now switch to the Main thread to talk to some STA object.
/// // Supposing it is also important to *not* do this step on our caller's callstack,
/// // be sure we yield even if we're on the UI thread.
/// await this.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true);
/// STAService.DoSomething();
/// }
///
///
///
public MainThreadAwaitable SwitchToMainThreadAsync(bool alwaysYield, CancellationToken cancellationToken = default(CancellationToken))
{
return new MainThreadAwaitable(this, this.Context.AmbientTask, cancellationToken, alwaysYield);
}
///
/// Runs the specified asynchronous method to completion while synchronously blocking the calling thread.
///
/// The asynchronous method to execute.
///
/// Any exception thrown by the delegate is rethrown in its original type to the caller of this method.
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
///
/// // On threadpool or Main thread, this method will block
/// // the calling thread until all async operations in the
/// // delegate complete.
/// joinableTaskFactory.Run(async delegate {
/// // still on the threadpool or Main thread as before.
/// await OperationAsync();
/// // still on the threadpool or Main thread as before.
/// await Task.Run(async delegate {
/// // Now we're on a threadpool thread.
/// await Task.Yield();
/// // still on a threadpool thread.
/// });
/// // Now back on the Main thread (or threadpool thread if that's where we started).
/// });
///
///
///
public void Run(Func asyncMethod)
{
this.Run(asyncMethod, JoinableTaskCreationOptions.None, entrypointOverride: null);
}
///
/// Runs the specified asynchronous method to completion while synchronously blocking the calling thread.
///
/// The asynchronous method to execute.
/// The used to customize the task's behavior.
public void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions)
{
this.Run(asyncMethod, creationOptions, entrypointOverride: null);
}
///
/// Runs the specified asynchronous method to completion while synchronously blocking the calling thread.
///
/// The type of value returned by the asynchronous operation.
/// The asynchronous method to execute.
/// The result of the Task returned by .
///
/// Any exception thrown by the delegate is rethrown in its original type to the caller of this method.
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
/// See the overload documentation for an example.
///
public T Run(Func> asyncMethod)
{
return this.Run(asyncMethod, JoinableTaskCreationOptions.None);
}
///
/// Runs the specified asynchronous method to completion while synchronously blocking the calling thread.
///
/// The type of value returned by the asynchronous operation.
/// The asynchronous method to execute.
/// The used to customize the task's behavior.
/// The result of the Task returned by .
///
/// Any exception thrown by the delegate is rethrown in its original type to the caller of this method.
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
public T Run(Func> asyncMethod, JoinableTaskCreationOptions creationOptions)
{
VerifyNoNonConcurrentSyncContext();
JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, creationOptions: creationOptions);
return joinable.CompleteOnCurrentThread();
}
///
/// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields.
/// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method
/// requires the main thread while the main thread is blocked waiting for the async method's completion.
///
/// The method that, when executed, will begin the async operation.
/// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary.
///
/// Exceptions thrown by the delegate are captured by the returned .
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
public JoinableTask RunAsync(Func asyncMethod)
{
return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: JoinableTaskCreationOptions.None);
}
///
/// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields.
/// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method
/// requires the main thread while the main thread is blocked waiting for the async method's completion.
///
/// The method that, when executed, will begin the async operation.
/// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary.
/// The used to customize the task's behavior.
///
/// Exceptions thrown by the delegate are captured by the returned .
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
public JoinableTask RunAsync(Func asyncMethod, JoinableTaskCreationOptions creationOptions)
{
return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: creationOptions);
}
///
/// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields.
/// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method
/// requires the main thread while the main thread is blocked waiting for the async method's completion.
///
/// The type of value returned by the asynchronous operation.
/// The method that, when executed, will begin the async operation.
///
/// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary.
///
///
/// Exceptions thrown by the delegate are captured by the returned .
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
public JoinableTask RunAsync(Func> asyncMethod)
{
return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: JoinableTaskCreationOptions.None);
}
///
/// Invokes an async delegate on the caller's thread, and yields back to the caller when the async method yields.
/// The async delegate is invoked in such a way as to mitigate deadlocks in the event that the async method
/// requires the main thread while the main thread is blocked waiting for the async method's completion.
///
/// The type of value returned by the asynchronous operation.
/// The method that, when executed, will begin the async operation.
/// The used to customize the task's behavior.
///
/// An object that tracks the completion of the async operation, and allows for later synchronous blocking of the main thread for completion if necessary.
///
///
/// Exceptions thrown by the delegate are captured by the returned .
/// When the delegate resumes from a yielding await, the default behavior is to resume in its original context
/// as an ordinary async method execution would. For example, if the caller was on the main thread, execution
/// resumes after an await on the main thread; but if it started on a threadpool thread it resumes on a threadpool thread.
///
public JoinableTask RunAsync(Func> asyncMethod, JoinableTaskCreationOptions creationOptions)
{
return this.RunAsync(asyncMethod, synchronouslyBlocking: false, creationOptions: creationOptions);
}
///
/// Responds to calls to
/// by scheduling a continuation to execute on the Main thread.
///
/// The callback to invoke.
internal SingleExecuteProtector RequestSwitchToMainThread(Action callback)
{
Requires.NotNull(callback, nameof(callback));
// Make sure that this thread switch request is in a job that is captured by the job collection
// to which this switch request belongs.
// If an ambient job already exists and belongs to the collection, that's good enough. But if
// there is no ambient job, or the ambient job does not belong to the collection, we must create
// a (child) job and add that to this job factory's collection so that folks joining that factory
// can help this switch to complete.
JoinableTask? ambientJob = this.Context.AmbientTask;
SingleExecuteProtector? wrapper = null;
if (ambientJob is null || (this.jobCollection is object && !this.jobCollection.Contains(ambientJob)))
{
JoinableTask? transient = this.RunAsync(
delegate
{
RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks.");
ambientJob = this.Context.AmbientTask;
wrapper = SingleExecuteProtector.Create(ambientJob, callback);
ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true);
return Task.CompletedTask;
},
synchronouslyBlocking: false,
creationOptions: JoinableTaskCreationOptions.None,
entrypointOverride: callback);
if (transient.Task.IsFaulted)
{
// rethrow the exception.
transient.Task.GetAwaiter().GetResult();
}
}
else
{
wrapper = SingleExecuteProtector.Create(ambientJob, callback);
ambientJob.Post(SingleExecuteProtector.ExecuteOnce, wrapper, true);
}
Assumes.NotNull(wrapper);
return wrapper;
}
///
/// Posts a callback to the main thread via the underlying dispatcher,
/// or to the threadpool when no dispatcher exists on the main thread.
///
internal void PostToUnderlyingSynchronizationContextOrThreadPool(SingleExecuteProtector callback)
{
Requires.NotNull(callback, nameof(callback));
if (this.UnderlyingSynchronizationContext is object)
{
this.PostToUnderlyingSynchronizationContext(SingleExecuteProtector.ExecuteOnce, callback);
}
else
{
ThreadPool.QueueUserWorkItem(SingleExecuteProtector.ExecuteOnceWaitCallback, callback);
}
}
/// Runs the specified asynchronous method.
/// The asynchronous method to execute.
/// The used to customize the task's behavior.
/// The delegate to record as the entrypoint for this JoinableTask.
internal void Run(Func asyncMethod, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride)
{
VerifyNoNonConcurrentSyncContext();
JoinableTask? joinable = this.RunAsync(asyncMethod, synchronouslyBlocking: true, creationOptions: creationOptions, entrypointOverride: entrypointOverride);
joinable.CompleteOnCurrentThread();
}
internal void Post(SendOrPostCallback callback, object? state, bool mainThreadAffinitized)
{
Requires.NotNull(callback, nameof(callback));
if (mainThreadAffinitized)
{
JoinableTask? transient = this.RunAsync(delegate
{
RoslynDebug.Assert(this.Context.AmbientTask is object, $"{nameof(this.Context.AmbientTask)} is always set for {nameof(this.RunAsync)} callbacks.");
this.Context.AmbientTask.Post(callback, state, true);
return Task.CompletedTask;
});
if (transient.Task.IsFaulted)
{
// rethrow the exception.
transient.Task.GetAwaiter().GetResult();
}
}
else
{
ThreadPool.QueueUserWorkItem(new WaitCallback(callback), state);
}
}
///
/// Posts a message to the specified underlying SynchronizationContext for processing when the main thread
/// is freely available.
///
/// The callback to invoke.
/// State to pass to the callback.
protected internal virtual void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state)
{
Requires.NotNull(callback, nameof(callback));
Assumes.NotNull(this.UnderlyingSynchronizationContext);
this.UnderlyingSynchronizationContext.Post(callback, state);
}
///
/// Raised when a joinable task has requested a transition to the main thread.
///
/// The task requesting the transition to the main thread.
///
/// This event may be raised on any thread, including the main thread.
///
protected internal virtual void OnTransitioningToMainThread(JoinableTask joinableTask)
{
Requires.NotNull(joinableTask, nameof(joinableTask));
}
///
/// Raised whenever a joinable task has completed a transition to the main thread.
///
/// The task whose request to transition to the main thread has completed.
/// A value indicating whether the transition was cancelled before it was fulfilled.
///
/// This event is usually raised on the main thread, but can be on another thread when is true.
///
protected internal virtual void OnTransitionedToMainThread(JoinableTask joinableTask, bool canceled)
{
Requires.NotNull(joinableTask, nameof(joinableTask));
}
///
/// Synchronously blocks the calling thread for the completion of the specified task.
/// If running on the main thread, any applicable message pump is suppressed
/// while the thread sleeps.
///
/// The task whose completion is being waited on.
///
/// Implementations should take care that exceptions from faulted or canceled tasks
/// not be thrown back to the caller.
///
protected internal virtual void WaitSynchronously(Task task)
{
if (this.Context.IsOnMainThread)
{
// Suppress any reentrancy by causing this synchronously blocking wait
// to not pump any messages at all.
using (this.Context.NoMessagePumpSynchronizationContext.Apply())
{
this.WaitSynchronouslyCore(task);
}
}
else
{
this.WaitSynchronouslyCore(task);
}
}
///
/// Synchronously blocks the calling thread for the completion of the specified task.
///
/// The task whose completion is being waited on.
///
/// Implementations should take care that exceptions from faulted or canceled tasks
/// not be thrown back to the caller.
///
protected virtual void WaitSynchronouslyCore(Task task)
{
Requires.NotNull(task, nameof(task));
int hangTimeoutsCount = 0; // useful for debugging dump files to see how many times we looped.
int hangNotificationCount = 0;
Guid hangId = Guid.Empty;
Stopwatch? stopWatch = null;
try
{
while (!task.Wait(this.HangDetectionTimeout))
{
if (hangTimeoutsCount == 0)
{
stopWatch = Stopwatch.StartNew();
}
hangTimeoutsCount++;
TimeSpan hangDuration = TimeSpan.FromMilliseconds(this.HangDetectionTimeout.TotalMilliseconds * hangTimeoutsCount);
if (hangId == Guid.Empty)
{
hangId = Guid.NewGuid();
}
if (!this.IsWaitingOnLongRunningTask())
{
hangNotificationCount++;
this.Context.OnHangDetected(hangDuration, hangNotificationCount, hangId);
}
}
if (hangNotificationCount > 0)
{
RoslynDebug.Assert(stopWatch is object);
// We detect a false alarm. The stop watch was started after the first timeout, so we add intial timeout to the total delay.
this.Context.OnFalseHangDetected(
stopWatch.Elapsed + this.HangDetectionTimeout,
hangId);
}
}
catch (AggregateException)
{
// Swallow exceptions thrown by Task.Wait().
// Our caller just wants to know when the Task completes,
// whether successfully or not.
}
}
///
/// Check whether the current joinableTask is waiting on a long running task.
///
/// Return true if the current synchronous task on the thread is waiting on a long running task.
protected bool IsWaitingOnLongRunningTask()
{
JoinableTask? currentBlockingTask = JoinableTask.TaskCompletingOnThisThread;
if (currentBlockingTask is object)
{
if ((currentBlockingTask.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning)
{
return true;
}
using (this.Context.NoMessagePumpSynchronizationContext.Apply())
{
var allJoinedJobs = new HashSet();
lock (this.Context.SyncContextLock)
{
JoinableTaskDependencyGraph.AddSelfAndDescendentOrJoinedJobs(currentBlockingTask, allJoinedJobs);
return allJoinedJobs.Any(t => (t.CreationOptions & JoinableTaskCreationOptions.LongRunning) == JoinableTaskCreationOptions.LongRunning);
}
}
}
return false;
}
///
/// Adds the specified joinable task to the applicable collection.
///
protected void Add(JoinableTask joinable)
{
Requires.NotNull(joinable, nameof(joinable));
if (this.jobCollection is object)
{
this.jobCollection.Add(joinable);
}
}
///
/// Throws an exception if an active AsyncReaderWriterLock
/// upgradeable read or write lock is held by the caller.
///
///
/// This is important to call from the Run and Run{T} methods because
/// if they are called from within an ARWL upgradeable read or write lock,
/// then Run will synchronously block while inside the semaphore held
/// by the ARWL that prevents concurrency. If the delegate within Run
/// yields and then tries to reacquire the ARWL lock, it will be unable
/// to re-enter the semaphore, leading to a deadlock.
/// Instead, callers who hold UR/W locks should never call Run, or should
/// switch to the STA thread first in order to exit the semaphore before
/// calling the Run method.
///
private static void VerifyNoNonConcurrentSyncContext()
{
// Don't use Verify.Operation here to avoid loading a string resource in success cases.
if (SynchronizationContext.Current is AsyncReaderWriterLock.NonConcurrentSynchronizationContext)
{
#if NETFRAMEWORK || NETCOREAPP // Assertion failures crash on .NET Core < 3.0
Report.Fail(Strings.NotAllowedUnderURorWLock); // pops a CHK assert dialog, but doesn't throw.
#endif
Verify.FailOperation(Strings.NotAllowedUnderURorWLock); // actually throws, even in RET.
}
}
///
/// Wraps the invocation of an async method such that it may
/// execute asynchronously, but may potentially be
/// synchronously completed (waited on) in the future.
///
/// The asynchronous method to execute.
/// A value indicating whether the launching thread will synchronously block for this job's completion.
/// The used to customize the task's behavior.
/// The entry method's info for diagnostics.
private JoinableTask RunAsync(Func asyncMethod, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions, Delegate? entrypointOverride = null)
{
Requires.NotNull(asyncMethod, nameof(asyncMethod));
var job = new JoinableTask(this, synchronouslyBlocking, creationOptions, entrypointOverride ?? asyncMethod);
this.ExecuteJob(asyncMethod, job);
return job;
}
private JoinableTask RunAsync(Func> asyncMethod, bool synchronouslyBlocking, JoinableTaskCreationOptions creationOptions)
{
Requires.NotNull(asyncMethod, nameof(asyncMethod));
var job = new JoinableTask(this, synchronouslyBlocking, creationOptions, asyncMethod);
this.ExecuteJob(asyncMethod, job);
return job;
}
private void ExecuteJob(Func asyncMethod, JoinableTask job)
{
try
{
using (var framework = new RunFramework(this, job))
{
Task asyncMethodResult;
try
{
asyncMethodResult = asyncMethod();
}
catch (Exception ex)
{
var tcs = new TaskCompletionSource();
tcs.SetException(ex);
asyncMethodResult = tcs.Task;
}
job.SetWrappedTask(asyncMethodResult);
}
}
catch (Exception ex) when (FailFast(ex))
{
// We use a crashing exception filter to capture all the detail possible (even before unwinding the callstack)
// when an exception is thrown from this critical method.
// In particular, we have seen the WeakReference object that is instantiated by "new RunFramework" throw OutOfMemoryException.
throw Assumes.NotReachable();
}
static bool FailFast(Exception ex)
{
Environment.FailFast("Unexpected exception thrown in critical scheduling code.", ex);
throw Assumes.NotReachable();
}
}
///
/// An awaitable struct that facilitates an asynchronous transition to the Main thread.
///
public readonly struct MainThreadAwaitable
{
private readonly JoinableTaskFactory? jobFactory;
private readonly JoinableTask? job;
private readonly CancellationToken cancellationToken;
private readonly bool alwaysYield;
///
/// Initializes a new instance of the struct.
///
internal MainThreadAwaitable(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 MainThreadAwaiter GetAwaiter()
{
if (this.jobFactory is null)
{
return default;
}
return new MainThreadAwaiter(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 static readonly Action