async、await的线程(上下文)逻辑

前言

请记住这两点:

1、默认情况下,await返回时总是会去恢复await前的上下文。

2、每一个应用程序都有自己的线程池和线程池上下文

UI程序的上下文

当一个方法被关键字async声明时,意味着该方法可异步执行,同时激活方法内的await关键字。

UI程序中只允许唯一的一个拥有UI上下文(SynchronizationContext)的线程运行。

例1

private async void Button1_Click(object sender, EventArgs e)
{
    Console.WriteLine($"Before WaitAsync => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
    await WaitAsync();
    Console.WriteLine($"After WaitAsync => Thread Id: {Thread.CurrentThread.ManagedThreadId}");

    async Task WaitAsync()
    {
        Console.WriteLine($"Before Task.Run => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
        await Task.Run(() => {
            Console.WriteLine($"await => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
        });
        Console.WriteLine($"After Task.Run => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
    }
}

//点击按钮,输出
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 3
//After Task.Run => Thread Id: 1
//After WaitAsync => Thread Id: 1

可以看到,除了WaitAsync里的await语句在另一线程执行,await前后的代码均在原线程,也就是UI线程执行。

接下来将await WaitAsync()改为WaitAsync().Wait()再执行

//点击按钮,输出
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 3

此时,程序将一直阻塞在await语句,原因是当使用Task.Wait() Result GetAwaiter() GetResult()等去获取结果时,await将阻塞当前线程,而当await内代码执行完后,await尝试恢复上下文,此时UI线程被阻塞,必须等await语句结束才能继续执行,因而造成死锁。

再将await语句改为

await Task.Run(() => {
    Console.WriteLine($"await => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
}).ConfigureAwait(false);

//点击按钮,输出
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 3
//After Task.Run => Thread Id: 3
//After WaitAsync => Thread Id: 1

ConfigureAwait(false)给Task配置了一个标记,使得在await内部代码执行结束后忽略之前的上下文,直接用当前线程执行async里await下面的代码,因而不会造成死锁。所以,精通异步编程的大佬有句话,“一旦开始使用async,最好全程使用它”。

接下来把代码恢复到最开始的版本,再将await语句改为

await Task.Run(() => {
    Console.WriteLine($"await => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
}).ContinueWith((task) => {
    Console.WriteLine($"ContinueWith => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
});

//多次点击按钮,输出
//1
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 3
//ContinueWith => Thread Id: 4
//After Task.Run => Thread Id: 1
//After WaitAsync => Thread Id: 1

//2
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 3
//ContinueWith => Thread Id: 3
//After Task.Run => Thread Id: 1
//After WaitAsync => Thread Id: 1

//3
//Before WaitAsync => Thread Id: 1
//Before Task.Run => Thread Id: 1
//await => Thread Id: 5
//ContinueWith => Thread Id: 5
//After Task.Run => Thread Id: 1
//After WaitAsync => Thread Id: 1

//...

多次测试说明,ContinueWith里的代码并不一定由执行await的线程执行,具体由哪一个线程执行在于TaskScheduler的调度。

ASP.NET Classic中的上下文

ASP.NET Classic程序(ASP.NET Core以前)使用的ASP.NET请求上下文跟UI程序的上下文略有不同,但结果实际上是类似的。

Debug.WriteLine($"Before WaitAsync => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
await WaitAsync();
Debug.WriteLine($"After WaitAsync => Thread Id: {Thread.CurrentThread.ManagedThreadId}");

async Task WaitAsync()
{
    Debug.WriteLine($"Before Task.Run => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
    await Task.Run(() => {
        Debug.WriteLine($"await => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
    });
    Debug.WriteLine($"After Task.Run => Thread Id: {Thread.CurrentThread.ManagedThreadId}");
}
//输出
//1
//Before WaitAsync => Thread Id: 7
//Before Task.Run => Thread Id: 7
//await => Thread Id: 6
//After Task.Run => Thread Id: 7
//After WaitAsync => Thread Id: 7

//2
//Before WaitAsync => Thread Id: 6
//Before Task.Run => Thread Id: 6
//await => Thread Id: 7
//After Task.Run => Thread Id: 7
//After WaitAsync => Thread Id: 7

//3
//Before WaitAsync => Thread Id: 9
//Before Task.Run => Thread Id: 9
//await => Thread Id: 14
//After Task.Run => Thread Id: 15
//After WaitAsync => Thread Id: 15

虽然ASP.NET在await之后使用的是线程池里的线程去恢复ASP.NET请求上下文,但该请求上下文依然不能被超过一个线程同时拥有。所以,如果将await WaitAsync()改为WaitAsync().Wait(),执行

// 结果
//Before WaitAsync => Thread Id: 6
//Before Task.Run => Thread Id: 6
//await => Thread Id: 7

//Before WaitAsync => Thread Id: 7
//Before Task.Run => Thread Id: 7
//await => Thread Id: 8

//Before WaitAsync => Thread Id: 8
//Before Task.Run => Thread Id: 8
//await => Thread Id: 9

//Before WaitAsync => Thread Id: 10
//Before Task.Run => Thread Id: 10
//await => Thread Id: 9

//Before WaitAsync => Thread Id: 11
//Before Task.Run => Thread Id: 11
//await => Thread Id: 9

//Before WaitAsync => Thread Id: 9
//Before Task.Run => Thread Id: 9
//await => Thread Id: 12

//Before WaitAsync => Thread Id: 12
//Before Task.Run => Thread Id: 12
//await => Thread Id: 13

//...

同样会造成原线程的阻塞(估计会造成内存泄漏),但不会造成整个ASP.NET应用的阻塞,应用会用线程池里的其他线程去响应接下来的请求。也就是说ASP.NET Classic应用会在await内部代码执行完成时,尝试用线程池内的线程(可能是await前的线程,也可能不是)去恢复上下文。

如果用ConfigureAwait(false)进行配置则不会去恢复请求上下文。

//结果
//1
//Before WaitAsync => Thread Id: 6
//Before Task.Run => Thread Id: 6
//await => Thread Id: 7
//After Task.Run => Thread Id: 7
//After WaitAsync => Thread Id: 6

//2
//Before WaitAsync => Thread Id: 8
//Before Task.Run => Thread Id: 8
//await => Thread Id: 6
//After Task.Run => Thread Id: 6
//After WaitAsync => Thread Id: 8

//3
//Before WaitAsync => Thread Id: 6
//Before Task.Run => Thread Id: 6
//await => Thread Id: 7
//After Task.Run => Thread Id: 7
//After WaitAsync => Thread Id: 6

ASP.NET Core应用和控制台程序

由于ASP.NET Core使用的是线程池上下文,该上下文由线程池内所有线程共享,所以不存在上述死锁或阻塞问题。同样地,控制台也不会出现类似的问题。

posted @ 2021-01-09 12:45  Dirt·in·firework  阅读(566)  评论(1)    收藏  举报