异步/等待:为什么后面等待的代码也同时在后台线程而不是在原始主线程上执行?

时间:2019-06-29 15:27:53

标签: .net multithreading async-await clr

下面是我的代码:

SELECT DAY
FROM RESERVE R
WHERE NOT EXISTS (
SELECT SID
FROM SAILOR S
EXCEPT
SELECT S.SID
FROM SAILOR S, RESERVE R
WHERE S.SID = R.SID)
GROUP BY DAY;

输出为

  

1

// 3秒后

  

3

     

完成工作!

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

3 个答案:

答案 0 :(得分:2)

异步入口点只是一个编译器技巧。在后台,编译器会生成以下实际入口点:

private static void <Main>(string[] args)
{
    _Main(args).GetAwaiter().GetResult();
}

如果将代码更改为这样:

class Program
{
    private static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }


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

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

您会得到的:

1
4
Done with work!
1

按预期,主线程正在等待工作完成。

答案 1 :(得分:1)

在您的代码中,一旦在此处调用await,主线程便结束:

string message = await DoWorkAsync();

DoWorkAsync()创建任务以来执行会有所不同,并且此调用之后的所有代码都将在新创建的任务中执行,因此调用await DoWorkAsync();后主线程无事可做将完成。

答案 2 :(得分:1)

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

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

  1. 如果SynchronizationContext.Current返回NULL,则 延续线程将默认为线程池线程
  2. 如果SynchronizationContext.Current不为NULL,则继续 将在捕获的上下文中执行。
  3. 并且:如果await用于后台线程(因此, 从后台线程启动后台线程),然后 SynchronizationContext将始终是线程池线程。

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

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

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

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

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

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

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

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