异步/等待:为什么后面等待的代码也同时在后台线程而不是在原始主线程上执行? [英] Async/ Await: why does the code that follows await is also executed on the background thread and not on the original primary thread?

查看:87
本文介绍了异步/等待:为什么后面等待的代码也同时在后台线程而不是在原始主线程上执行?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

下面是我的代码:

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        string message = await DoWorkAsync();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }

    static async Task<string> DoWorkAsync()
    {
        return await Task.Run(() =>
        {
            Thread.Sleep(3_000);
            return "Done with work!";
        });
    }
}

输出为

1

//3秒后

3

完成工作!

所以您可以看到主线程(id为1)更改为工作线程(id为3),那么主线程怎么会消失呢?

so you can see the main thread(id is 1) changed to worker thread(id is 3), so how come the main thread just disappear?

推荐答案

这是您选择的应用程序类型的结果.控制台应用程序和GUI应用程序在SynchronizationContext方面的行为有所不同.使用await时,将捕获当前的SynchronizationContext并将其传递给后台线程.
这个想法不是通过等待后台线程完成来阻塞主线程.其余代码排队,当前上下文存储在背景线程将捕获的SynchronizationContext中.后台线程完成后,它返回捕获的SynchronizationContext,以便排队的剩余代码可以恢复执行.您可以通过访问SynchronizationContext.Current属性来获取当前上下文.等待await完成的代码(await之后的其余代码)将作为继续,并在捕获的SynchronizationContext上执行.

This is a result of application type you chose. Console apps and GUI apps behave differently regarding the SynchronizationContext. When you use await, then the current SynchronizationContext is captured and passed to the background thread.
The idea is not to block the main thread by just waiting for the background thread to complete. The remaining code is enqueued and the current context stored in the SynchronizationContext which the backgroung thread wil capture. When the background thread completes, it returns the captured SynchronizationContext so that the enqueued remaining code can resume execution. You can get the current context by accessing SynchronizationContext.Current property. The code that is waiting for await to finish (the remaining code after await) will be enqueued as a continuation and executed on the captured SynchronizationContext.

SynchronizationContext.Current的默认值是用于GUI应用程序的UI线程,例如WPF或用于控制台应用程序的NULL.控制台应用程序没有SynchronizationContext,因此为了能够使用async,框架使用了ThreadPool SynchronizationContext. SynchronizationContext行为的规则是

The default value of SynchronizationContext.Current is the UI thread for GUI applications like WPF or NULL for console applications. Console applications don't have a SynchronizationContext, so to be able to use async, the framwork uses the ThreadPool SynchronizationContext. The rules for the SynchronizationContext behaviour is that

  1. 如果SynchronizationContext.Current返回NULL,则 延续线程将默认为线程池线程
  2. 如果SynchronizationContext.Current不为NULL,则继续 将在捕获的上下文中执行.
  3. 并且:如果await用于后台线程(因此, 后台线程从后台线程启动),然后 SynchronizationContext将始终是线程池线程.
  1. If the SynchronizationContext.Current returns NULL, the continuation thread will default to a thread pool thread
  2. If SynchronizationContext.Current is not NULL, the continuation will be executed on the captured context.
  3. And: if the await is used on a background thread (hence a new background thread is started from a background thread), then the SynchronizationContext will always be a thread pool thread.

场景1,控制台应用程序:
规则1)适用:线程1 调用await,它将尝试捕获当前上下文. await将使用ThreadPool中的后台线程线程3 执行异步委托.
委托完成后,调用线程的其余代码将在捕获的上下文中执行.由于此上下文在控制台应用程序中为NULL,因此默认SynchronizationContext将生效(第一条规则).因此,调度程序决定继续在ThreadPool线程线程3 上执行(出于效率考虑.上下文切换非常昂贵).

Scenario 1, a console application:
rule 1) applies: thread 1 calls await which will try to capture the current context. await will use the background thread thread 3 from the ThreadPool to execute the asynchronous delegate.
Once the delegate completes, the remaining code of the calling thread will execute on the captured context. Since this context is NULL in console applications, the default SynchronizationContext will take effect (first rule). Therefore the scheduler decides to continue execution on the ThreadPool thread thread 3 (for efficiency. Context switches are expensive).

方案2,GUI应用程序:
规则2)适用:线程1 调用await,它将尝试捕获当前上下文(UI SynchronizationContext). await将使用ThreadPool中的后台线程线程3 执行异步委托.
委托完成后,调用线程的其余代码将在捕获的上下文中执行,即UI SynchronizationContext 线程1 .

Scenario 2, a GUI application:
rule 2) applies: thread 1 calls await which will try to capture the current context (the UI SynchronizationContext). await will use the background thread thread 3 from the ThreadPool to execute the asynchronous delegate.
Once the delegate completes, the remaining code of the calling thread will execute on the captured context, the UI SynchronizationContext thread 1.

场景3,一个GUI应用程序和Task.ContinueWith:
规则2)和规则3)适用:线程1 调用await,它将尝试捕获当前上下文(UI SynchronizationContext). await将使用ThreadPool中的后台线程线程3 执行异步委托.委托完成后,继续TaskContinueWith.由于我们仍在后台线程上,因此将新的TreadPool线程线程4 与捕获的线程3 SynchronizationContext一起使用.一旦继续操作完成,上下文将返回到线程3 ,它将在捕获的SynchronizationContext(即UI线程线程1 )上执行调用者的其余代码.

Scenario 3, a GUI application and Task.ContinueWith:
rule 2) and rule 3) applies: thread 1 calls await which will try to capture the current context (the UI SynchronizationContext). await will use the background thread thread 3 from the ThreadPool to execute the asynchronous delegate. Once the delegate completes, the continuation TaskContinueWith. Since we are still on the background thread, a new TreadPool thread thread 4 is used with the captured SynchronizationContext of thread 3. Once the continuation completes the context returns to thread 3 which will execute the remaining code of the caller on the captured SynchronizationContext which is the UI thread thread 1.

场景4,GUI应用程序和Task.ConfigureAwait(false)(await DoWorkAsync().ConfigureAwait(false);):
规则1)适用:线程1 调用await并在ThreadPool后台线程线程3 上执行异步委托.但是由于该任务是使用Task.ConfigureAwait(false) 线程3 配置的,因此无法捕获调用方的SynchronizationContext(UI SynchronizationContext).因此,线程3的SynchronizationContext.Current属性将为NULL,并且将应用默认的SynchronizationContext:上下文将为ThreadPool线程.由于性能优化(上下文切换非常昂贵),因此上下文将成为线程3 的当前SynchronizationContext.这意味着一旦线程3 完成,则将在默认的SynchronizationContext 线程3 上执行hte调用程序的其余代码. Task.ConfigureAwait的默认值为true,它可以捕获呼叫方SynchronizationContext.

Scenario 4, a GUI application and Task.ConfigureAwait(false) (await DoWorkAsync().ConfigureAwait(false);):
rule 1) applies: thread 1 calls await and executes the asynchronous delegate on a ThreadPool background thread thread 3. But because the task was configured with Task.ConfigureAwait(false) thread 3 doesn't capture the SynchronizationContext of the caller (UI SynchronizationContext). The SynchronizationContext.Current property of thread 3 will therefore be NULL and the default SynchronizationContext applies: the context will be a ThreadPool thread. Because of performance optimizations (context switching is expensive) the context will be the current SynchronizationContext of thread 3. This means that once thread 3 completes, the remaining code of hte caller will be executed on the default SynchronizationContext thread 3. The default Task.ConfigureAwait value is true, which enables capturing of the caller SynchronizationContext.

场景5,GUI应用程序和Task.WaitTask.ResultTask.GetAwaiter.GetResult:
规则2适用,但应用程序将死锁.捕获当前线程1 SynchronizationContext.但是由于异步委托是同步执行的(Task.WaitTask.ResultTask.GetAwaiter.GetResult会将异步操作转换为委托的同步执行),所以线程1 将一直阻塞直到现在同步委托完成.
由于该代码是同步执行的,因此 thread 1 的其余代码不会作为 thread 3 的延续入队,因此,一旦 thread 1 执行,委托完成.现在,在线程3 上的委托已完成,它不能将线程1 SynchronizationContext返回到线程1 ,因为线程1 仍在阻塞(并因此锁定了SynchronizationContext). 线程3 将无限期等待线程1 释放对SynchronizationContext的锁定,从而使线程1 无限期等待线程3 返回->死锁.

Scenario 5, a GUI application and Task.Wait, Task.Result or Task.GetAwaiter.GetResult:
rule 2 applies but the application will deadlock. The current SynchronizationContext of thread 1 is captured. But because the asynchronous delegate is executed synchronously (Task.Wait, Task.Result or Task.GetAwaiter.GetResult will turn the asynchronous operation into a synchronous execution of the delegate), thread 1 will block until the now synchronous delegate completes.
Since the code is executed synchronously the remaining code of thread 1 was not enqueued as continuation of thread 3 and will therefore execute on thread 1 once the delegate completes. Now that the delegate on thread 3 completes it cannot return the SynchronizationContext of thread 1 to thread 1, because thread 1 is still blocking (and thus locking the SynchronizationContext). Thread 3 will wait infinitely for thread 1 to release the lock on SynchronizationContext, which in turn makes thread 1 wait infinitely for thread 3 to return --> deadlock.

场景6,控制台应用程序和Task.WaitTask.ResultTask.GetAwaiter.GetResult:
规则1适用.捕获当前线程1 SynchronizationContext.但是,因为这是一个控制台应用程序,所以上下文为NULL,并且默认为SynchronizationContext.异步委托在ThreadPool后台线程线程3 线程1上同步执行(Task.WaitTask.ResultTask.GetAwaiter.GetResult会将异步操作转换为同步操作). 将一直阻塞,直到线程3 上的委托完成为止.由于代码是同步执行的,因此剩余的代码不会作为 thread 3 的延续入队,因此一旦委托完成,它将在 thread 1 上执行.在控制台应用程序的情况下,没有死锁情况,因为线程1 SynchronizationContext为NULL,并且线程3 必须使用默认上下文.

Scenario 6, console application and Task.Wait, Task.Result or Task.GetAwaiter.GetResult:
rule 1 applies. The current SynchronizationContext of thread 1 is captured. But because this is a console application the context is NULL and the default SynchronizationContext applies. The asynchronous delegate is executed synchronously (Task.Wait, Task.Result or Task.GetAwaiter.GetResult will turn the asynchronous operation into a synchronous operation) on a ThreadPool background thread thread 3 and thread 1 will block until the delegate on thread 3 completes. Since the code is executed synchronously the remaining code was not enqueued as continuation of thread 3 and will therefore execute on thread 1 once the delegate completes. No deadlock situation in case of a console aplication, since the SynchronizationContext of thread 1 was NULL and thread 3 has to use the default context.

您的示例代码与方案1匹配.这是因为您正在运行控制台应用程序,而默认的SynchronizationContext则适用,因为控制台应用程序的SynchronizationContext始终为NULL.当捕获的SynchronizationContext为NULL时,Task使用默认上下文,它是ThreadPool的线程.由于异步委托已经在ThreadPool线程上执行,因此TaskScheduler决定留在该线程上,并因此在线程3中执行调用者线程 thread 1 的排队剩余代码. .

The code of your example matches scenario 1. It#s because you are running a console application and the default SynchronizationContext that applies because the SynchronizationContext of console applications is always NULL. When the captured SynchronizationContext is NULL, Task uses the default context which is a thread of the ThreadPool. Since the asynchronous delegate is already executed on a ThreadPool thread the TaskScheduler decides to stay on this thread and therefore to execute the enqueued remaining code of the caller thread thread 1 in thread 3.

在GUI应用程序中,最佳实践是始终在所有地方都使用Task.ConfigureAwait(false),除非您明确想要捕获调用方的SynchronizationContext.这样可以防止应用程序意外死锁.

In GUI applications it's best practice to always use Task.ConfigureAwait(false) everywhere except you explicitly want to capture the SynchronizationContext of the caller. This will prevent accidental deadlocks in you application.

这篇关于异步/等待:为什么后面等待的代码也同时在后台线程而不是在原始主线程上执行?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆