Adding an API That Allows Switching Back to the First Calling Thread When Awaited #114694
Replies: 2 comments 2 replies
-
This is already the default behavior of The original thread must support to be switched to, which needs a
It's up to the |
Beta Was this translation helpful? Give feedback.
-
.NET does not provide an API that lets you run code on other threads without their corporation. However it does provide at least two abstractions that effect how asynchronous methods are scheduled on threads: // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Threading;
using System.Runtime.CompilerServices;
namespace System
{
public static class SynchronizationContextExtensions
{
public static Austin.YieldToSyncContextAwaitable Yield(this SynchronizationContext syncCtx)
{
return new Austin.YieldToSyncContextAwaitable(syncCtx);
}
}
}
namespace Austin
{
/// <summary>Provides an awaitable context for switching into a target environment.</summary>
/// <remarks>This type is intended for compiler use only.</remarks>
public readonly struct YieldToSyncContextAwaitable
{
readonly SynchronizationContext syncCtx;
public YieldToSyncContextAwaitable(SynchronizationContext syncCtx)
{
ArgumentNullException.ThrowIfNull(syncCtx);
this.syncCtx = syncCtx;
}
/// <summary>Gets an awaiter for this <see cref="YieldAwaitable"/>.</summary>
/// <returns>An awaiter for this awaitable.</returns>
/// <remarks>This method is intended for compiler use rather than use directly in code.</remarks>
public YieldToSyncContextAwaiter GetAwaiter() { return new YieldToSyncContextAwaiter(syncCtx); }
/// <summary>Provides an awaiter that switches into a target environment.</summary>
/// <remarks>This type is intended for compiler use only.</remarks>
public readonly struct YieldToSyncContextAwaiter : ICriticalNotifyCompletion
{
readonly SynchronizationContext syncCtx;
public YieldToSyncContextAwaiter(SynchronizationContext syncCtx)
{
ArgumentNullException.ThrowIfNull(syncCtx);
this.syncCtx = syncCtx;
}
/// <summary>Gets whether a yield is not required.</summary>
/// <remarks>This property is intended for compiler user rather than use directly in code.</remarks>
public bool IsCompleted => false; // yielding is always required for YieldAwaiter, hence false
/// <summary>Posts the <paramref name="continuation"/> back to the current context.</summary>
/// <param name="continuation">The action to invoke asynchronously.</param>
/// <exception cref="ArgumentNullException">The <paramref name="continuation"/> argument is null (<see langword="Nothing" /> in Visual Basic).</exception>
public void OnCompleted(Action continuation)
{
syncCtx.Post(s_sendOrPostCallbackRunAction, continuation);
}
/// <summary>Posts the <paramref name="continuation"/> back to the current context.</summary>
/// <param name="continuation">The action to invoke asynchronously.</param>
/// <exception cref="ArgumentNullException">The <paramref name="continuation"/> argument is null (<see langword="Nothing" /> in Visual Basic).</exception>
public void UnsafeOnCompleted(Action continuation)
{
syncCtx.Post(s_sendOrPostCallbackRunAction, continuation);
}
/// <summary>SendOrPostCallback that invokes the Action supplied as object state.</summary>
private static readonly SendOrPostCallback s_sendOrPostCallbackRunAction = RunAction;
/// <summary>Runs an Action delegate provided as state.</summary>
/// <param name="state">The Action delegate to invoke.</param>
private static void RunAction(object? state) { ((Action)state!)(); }
/// <summary>Ends the await operation.</summary>
public void GetResult() { } // Nop. It exists purely because the compiler pattern demands it.
}
}
} You can use it like this: // get sync context.
// Typically you would want to get the sync context by calling SynchronizationContext.Current
// to get the relevant context for the UI framework you are using
using System.Collections.Concurrent;
var syncCtx = new MySyncContext();
//...
// some time later, maybe on a different thread,
// yield back to
Console.WriteLine($"Currently on thread id {Thread.CurrentThread.ManagedThreadId} named: {Thread.CurrentThread.Name}");
await syncCtx.Yield();
Console.WriteLine($"After awaiting, currently on thread id {Thread.CurrentThread.ManagedThreadId} named: {Thread.CurrentThread.Name}");
class MySyncContext : SynchronizationContext
{
readonly BlockingCollection<(SendOrPostCallback, object?, ManualResetEvent?)> _queue;
readonly Thread _thread;
public MySyncContext()
{
_queue = new BlockingCollection<(SendOrPostCallback, object?, ManualResetEvent?)>();
_thread = new Thread(ProcessQueue)
{
IsBackground = true,
Name = "MySyncContext thread"
};
_thread.Start();
}
private void ProcessQueue()
{
SynchronizationContext.SetSynchronizationContext(this);
while (true)
{
var (callback, state, waitHandle) = _queue.Take();
callback(state);
waitHandle?.Set();
}
}
public override void Post(SendOrPostCallback d, object? state)
{
_queue.Add((d, state, null));
}
public override void Send(SendOrPostCallback d, object? state)
{
using var waitHandle = new ManualResetEvent(false);
_queue.Add((d, state, waitHandle));
waitHandle.WaitOne();
}
} When I run this, I see:
Note how after awaiting |
Beta Was this translation helpful? Give feedback.
-
There's an interesting API in UniTask where, after awaiting it, subsequent content is scheduled to execute in the main loop. I want to create a new method for Task that, when called, retrieves the calling thread, records and stores it, and returns an object. When awaiting this object, subsequent content should be scheduled to execute on the stored thread.
Question: If the recorded thread has already been destroyed, how does the execution proceed?
Beta Was this translation helpful? Give feedback.
All reactions