为什么 awating 之后的代码在不同的线程上运行(即使设置了 SynchronizationContext)?

Moh*_*din 5 .net c# async-await

我正在执行以下代码片段来测试如何更改线程,在该线程上将await调用 ing 之后的代码。根据@Stephen Cleary 在这个答案中,为了能够await在同一线程(上下文)上继续执行异步代码,我需要设置SynchronizationContext,并且我这样做了,但是,我的代码在不同的线程中继续运行。

static void Main(string[] args)
{
    var mainSyncContex = new SynchronizationContext();
    SynchronizationContext.SetSynchronizationContext(mainSyncContex);

    Console.WriteLine($"Hello World! ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 1

    try
    {
        Task.Run(async () =>
        {
            SynchronizationContext.SetSynchronizationContext(mainSyncContex);

            Console.WriteLine($"Is there Sync Contex?: {SynchronizationContext.Current != null}");

            Console.WriteLine($"Before delay. ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 3
            await Task.Delay(1000).ConfigureAwait(true);
            Console.WriteLine($"After delay. ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 4
            throw new Exception();
        });
    }
    catch (Exception e)
    {
        Console.WriteLine($"Exception: {e.Message} Catch. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
    }

    Console.WriteLine($"Ending ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // <-- In thread 1
    Console.ReadKey();
}
Run Code Online (Sandbox Code Playgroud)

输出:

你好世界!ThreadId: 1
结束ThreadId: 1
是否有Sync Contex?:True
在延迟之前。ThreadId: 3
延迟后。线程 ID:4

为什么会发生这种情况?

Moh*_*din 4

我想根据我的理解展示一些代码,希望它可以帮助别人。

正如 aepot、dymanoid 和 Hans Passant(感谢他们)所说,使用默认值SynchronizationContext只会Postawaiting 到SynchronizationContext.

我创建了一个非常非常基本但不是最佳的SynchronizationContext来演示基本实现应该是什么样子。我的实现将创建一个新的并在同一个新创建的特定上下文中Thread运行一些s 。TaskThread

更好的实现(但非常复杂)可以在Stephen Cleary 的 GitHub 存储库中找到。

我的实现基本上如下所示(来自我的 GitHub 存储库,存储库中的代码将来可能会有所不同):

/// <summary>
/// This <see cref="SynchronizationContext"/> will call all posted callbacks in a single new thread.
/// </summary>
public class SingleNewThreadSynchronizationContext : SynchronizationContext
{
    readonly Thread _workerThread;
    readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _actionStatePairs = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

    /// <summary>
    /// Returns the Id of the worker <see cref="Thread"/> created by this <see cref="SynchronizationContext"/>.
    /// </summary>
    public int ManagedThreadId => _workerThread.ManagedThreadId;

    public SingleNewThreadSynchronizationContext()
    {
        // Creates a new thread to run the posted calls.
        _workerThread = new Thread(() =>
        {
            try
            {
                while (true)
                {
                    var actionStatePair = _actionStatePairs.Take();
                    SetSynchronizationContext(this);
                    actionStatePair.Key?.Invoke(actionStatePair.Value);
                }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine($"The thread {_workerThread.ManagedThreadId} of {nameof(SingleNewThreadSynchronizationContext)} was aborted.");
            }
        });

        _workerThread.IsBackground = true;
        _workerThread.Start();
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        // Queues the posted callbacks to be called in this SynchronizationContext.
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException();
    }

    public override void OperationCompleted()
    {
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(new SendOrPostCallback(_ => _workerThread.Abort()), null));
        _actionStatePairs.CompleteAdding();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个使用它的演示:

static void SingleNewThreadSynchronizationContextDemo()
{
    var synchronizationContext = new SingleNewThreadSynchronizationContext();

    // Creates some tasks to test that the whole calls in the tasks (before and after awaiting) will be called in the same thread.
    for (int i = 0; i < 20; i++)
        Task.Run(async () =>
        {
            SynchronizationContext.SetSynchronizationContext(synchronizationContext);
            // Before yielding, the task will be started in some thread-pool thread.
            var threadIdBeforeYield = Thread.CurrentThread.ManagedThreadId;
            // We yield to post the rest of the task after await to the SynchronizationContext.
            // Other possiblity here is maybe to start the whole Task using a different TaskScheduler.
            await Task.Yield();

            var threadIdBeforeAwait1 = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(100);
            var threadIdBeforeAwait2 = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(100);

            Console.WriteLine($"SynchronizationContext: thread Id '{synchronizationContext.ManagedThreadId}' | type '{SynchronizationContext.Current?.GetType()}.'");
            Console.WriteLine($"Thread Ids: Before yield '{threadIdBeforeYield}' | Before await1 '{threadIdBeforeAwait1}' | Before await2 '{threadIdBeforeAwait2}' | After last await '{Thread.CurrentThread.ManagedThreadId}'.{Environment.NewLine}");
        });
}

static void Main(string[] args)
{
    Console.WriteLine($"Entry thread {Thread.CurrentThread.ManagedThreadId}");
    SingleNewThreadSynchronizationContextDemo();
    Console.WriteLine($"Exit thread {Thread.CurrentThread.ManagedThreadId}");

    Console.ReadLine();
}
Run Code Online (Sandbox Code Playgroud)

输出:

/// <summary>
/// This <see cref="SynchronizationContext"/> will call all posted callbacks in a single new thread.
/// </summary>
public class SingleNewThreadSynchronizationContext : SynchronizationContext
{
    readonly Thread _workerThread;
    readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _actionStatePairs = new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

    /// <summary>
    /// Returns the Id of the worker <see cref="Thread"/> created by this <see cref="SynchronizationContext"/>.
    /// </summary>
    public int ManagedThreadId => _workerThread.ManagedThreadId;

    public SingleNewThreadSynchronizationContext()
    {
        // Creates a new thread to run the posted calls.
        _workerThread = new Thread(() =>
        {
            try
            {
                while (true)
                {
                    var actionStatePair = _actionStatePairs.Take();
                    SetSynchronizationContext(this);
                    actionStatePair.Key?.Invoke(actionStatePair.Value);
                }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine($"The thread {_workerThread.ManagedThreadId} of {nameof(SingleNewThreadSynchronizationContext)} was aborted.");
            }
        });

        _workerThread.IsBackground = true;
        _workerThread.Start();
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        // Queues the posted callbacks to be called in this SynchronizationContext.
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotSupportedException();
    }

    public override void OperationCompleted()
    {
        _actionStatePairs.Add(new KeyValuePair<SendOrPostCallback, object>(new SendOrPostCallback(_ => _workerThread.Abort()), null));
        _actionStatePairs.CompleteAdding();
    }
}
Run Code Online (Sandbox Code Playgroud)