C# 编译器如何知道何时切断异步方法?

Posted

技术标签:

【中文标题】C# 编译器如何知道何时切断异步方法?【英文标题】:How does a C# compiler know when to cut off an async method? 【发布时间】:2017-11-12 17:14:18 【问题描述】:

我试图了解更多关于async/await 的信息,特别是编译器如何知道在async 方法和await 处“暂停”而不产生额外的线程。

例如,假设我有一个 async 方法,例如

DoSomeStuff();
await sqlConnection.OpenAsync();
DoSomeOtherStuff();

我知道await sqlConnection.OpenAsync(); 是我的方法被“挂起”的地方,调用它的线程返回线程池,一旦跟踪连接打开的Task 完成,就会发现一个可用线程运行@ 987654329@.

| DoSomeStuff() | "start" opening connection | ------------------------------------ | 
| ---------------------------------------------------------- | DoSomeOtherStuff() - |

这就是我感到困惑的地方。我看OpenAsync(https://referencesource.microsoft.com/#System.Data/System/Data/Common/DBConnection.cs,e9166ee1c5d11996,references)的源码是

    public Task OpenAsync() 
        return OpenAsync(CancellationToken.None);
    

    public virtual Task OpenAsync(CancellationToken cancellationToken) 
        TaskCompletionSource<object> taskCompletionSource = new TaskCompletionSource<object>();

        if (cancellationToken.IsCancellationRequested) 
            taskCompletionSource.SetCanceled();
        
        else 
            try 
                Open();
                taskCompletionSource.SetResult(null);
            
            catch (Exception e) 
                taskCompletionSource.SetException(e);
            
        

        return taskCompletionSource.Task;
    

我想看到某个地方编译器会知道“切断”线程,因为任务已经开始与外部资源通信,但我并没有真正看到,事实上,Open(); 似乎暗示它正在同步等待。有人能解释一下这如何变成无线程的“真正的异步”代码吗?

【问题讨论】:

好吧,这样的 OpenAsync 实现实际上并不是“真正的异步”代码。 C#编译器重写你的代码,它变成了一个隐藏类的两个方法。 DoSomeOtherStuff() 调用在第二种方法中。您可能拥有并在这两个部分中使用的任何局部变量都将成为该类的字段。您可以使用 ildasm.exe 实用程序查看。 @CodeFuller 你能指出一个 OpenAsync() 的“真正异步”实现的例子吗?或者说明如何将上面的OpenAsync() 写成真正的异步? 【参考方案1】:

您的方法不一定会在等待时“暂停”。如果您正在等待的任务已经完成(您提供的代码中的情况) - 方法将照常继续。您正在查看的方法实际上不是SqlConnection 使用的方法,因为DbConnection 是基类而方法OpenAsync 是虚拟的。 SqlConnection 覆盖它并提供真正的异步实现。但是,并非所有提供商都这样做,而那些不这样做的提供商确实会使用您在问题中显示的实现。

当使用这样的实现时 - 整个事情将同步运行而无需任何线程切换。假设你有

public async Task Do() 
    DoSomeStuff();
    await sqlConnection.OpenAsync();
    DoSomeOtherStuff();

并且您使用的提供程序不提供OpenAsync 的真正异步版本。然后当有人调用await Do() - 调用线程将执行所有工作(DoSomeStuffOpenAsyncDoSomeOtherStuff)。如果那是 UI 线程 - 它将在整个持续时间内被阻塞(当人们在 UI 线程中为此类提供程序使用“异步”方法时,通常会发生这种情况,假设这会以某种方式将工作从 UI 线程中移出,但这种情况不会发生)。

【讨论】:

你能给我举一个 OpenAsync() 的“真正异步”实现的例子吗? @Questionaire 这里是SqlConnection的源代码:referencesource.microsoft.com/#System.Data/System/Data/… @Questionaire 真正的异步主要是异步 IO(文件、套接字)。您可能有兴趣阅读以下内容:blog.stephencleary.com/2013/11/there-is-no-thread.html【参考方案2】:

使用 async-await 的好处在于调用以下线程的线程

await sqlConnection.OpenAsync();

将被释放,并且可以从所属的线程池中使用。如果我们讨论的是 ASP.NET 应用程序,线程将被释放并可用于服务另一个传入的 HTTP 请求。这样,ASP.NET 线程池的线程将始终可用于服务 HTTP 请求,并且不会阻塞例如 I/O,例如打开与数据库的连接并执行某些 SQL 语句。

更新

这里需要说明的是,如果你要去await的任务已经完成,你的代码会同步运行。

【讨论】:

您的第二段 关于 Open() 具有误导性,因为 Open() 不需要在后台线程上运行。所示的实现是同步的,它本身不会改变上下文。 OpenAsyncOpen 在同一个上下文中运行,并且会阻塞与调用关联的单个线程。 @JSteward 我不确定你的论点是否正确。正如这里所说的docs.microsoft.com/en-us/dotnet/csharp/language-reference/… await 表达式不会阻塞它正在执行的线程。相反,它会导致编译器注册 async 方法的其余部分作为等待任务的延续。然后控制权返回给异步方法的调用者。当任务完成时,它调用它的继续,并且异步方法的执行从它停止的地方继续。 Task 中等待的整个代码将在单独的线程中运行。 只有当Task/Task &lt;T&gt;返回方法的实现产生并代表一个真正的异步方法时,该行为才适用。如果实现是同步和阻塞的; await 不会自动将工作移动到另一个上下文。无论可等待的返回类型如何,阻塞实现将始终阻塞。 @JSteward 我完全删除了Regarding the Open 的相应部分,因为它看起来有点误导。顺便说一句,我发现了一个很好的帖子,我想分享它是***.com/questions/37419572/…。谢谢

以上是关于C# 编译器如何知道何时切断异步方法?的主要内容,如果未能解决你的问题,请参考以下文章

异步在 C# 中如何工作?

C# 中的 C++ std::async 与异步/等待

如何让 UIPageViewController 知道我的异步下载何时完成?

你所不知道的 C# 中的细节

如何使用嵌入在 C++ 中的单声道编译 C# 代码?

C# 线程异步处理