在 Unity / C# 中,.Net 的 async/await 是不是从字面上开始另一个线程?

Posted

技术标签:

【中文标题】在 Unity / C# 中,.Net 的 async/await 是不是从字面上开始另一个线程?【英文标题】:In Unity / C#, does .Net's async/await start, literally, another thread?在 Unity / C# 中,.Net 的 async/await 是否从字面上开始另一个线程? 【发布时间】:2019-03-29 13:04:50 【问题描述】:

对于在 Unity 中研究这个困难主题的人来说非常重要

请务必查看我提出的另一个引发相关关键问题的问题:

In Unity specifically, "where" does an await literally return to?


对于 C# 专家来说,Unity 是单线程的1

在另一个线程上进行计算等是很常见的。

当你在另一个线程上做某事时,你经常使用 async/wait,因为,嗯,所有优秀的 C# 程序员都说这是做这件事的简单方法!

void TankExplodes() 

    ShowExplosion(); .. ordinary Unity thread
    SoundEffects(); .. ordinary Unity thread
    SendExplosionInfo(); .. it goes to another thread. let's use 'async/wait'


using System.Net.WebSockets;
async void SendExplosionInfo() 

    cws = new ClientWebSocket();
    try 
        await cws.ConnectAsync(u, CancellationToken.None);
        ...
        Scene.NewsFromServer("done!"); // class function to go back to main tread
    
    catch (Exception e)  ... 

好的,所以当你这样做时,当你在 Unity/C# 中以更传统的方式启动线程时,你会“像往常一样”做所有事情(所以使用 Thread 或其他任何东西或让一个本地插件或操作系统或任何可能的情况)。

一切都很好。

作为一个只懂 C# 的蹩脚 Unity 程序员,我一直假设上面的 async/await 模式实际上会启动另一个线程.

事实上,上面的代码是否真的启动了另一个线程,或者当您使用 natty async/wait 模式时,c#/.Net 是否使用其他方法来完成任务?

也许它在 Unity 引擎中的工作方式与“一般使用 C#”不同或具体? (IDK?)

请注意,在 Unity 中,它是否是线程会极大地影响您处理后续步骤的方式。于是就有了这个问题。


问题:我知道有很多关于“正在等待线程”的讨论,但是,(1)我从未在 Unity 设置中看到过这个讨论/回答 (这有什么区别吗?IDK?)(2)我从来没有看到一个明确的答案!


1一些辅助计算(例如,物理等)在其他线程上完成,但实际的“基于帧的游戏引擎”是一个纯线程。 (以任何方式“访问”主引擎框架线程都是不可能的:例如,在编写本机插件或在另一个线程上进行某些计算时,您只需在引擎框架线程上为组件留下标记和值以查看和在它们运行每一帧时使用。)

【问题讨论】:

这取决于平台和脚本后端。 @0xBFE1A8 啊我明白了。令人着迷! ios Unity 上使用 GCD 库。 嗯,你认为单声道会下降那么深吗?说 Windows 现在构建它;是 iL2CPP 后端吗? 因此大部分关于Unity线程的写法都是错误的! 【参考方案1】:

此阅读:Tasks are (still) not threads and async is not parallel 可能会帮助您了解幕后发生的事情。 简而言之,为了让您的任务在单独的线程上运行,您需要调用

Task.Run(()=>// the work to be done on a separate thread. ); 

然后您可以在任何需要的地方等待该任务。

回答你的问题

"事实上,上面的代码是真的启动了另一个线程,还是 c#/.Net 使用其他一些方法来实现任务时使用 漂亮的异步/等待模式?”

不 - 它没有。

如果你这样做了

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

然后cws.ConnectAsync(u, CancellationToken.None) 将在单独的线程上运行。

作为对评论的回答,这里是修改了更多解释的代码:

    async void SendExplosionInfo() 

        cws = new ClientWebSocket();
        try 
            var myConnectTask = Task.Run(()=>cws.ConnectAsync(u, CancellationToken.None));

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // class function to go back to main tread
        
        catch (Exception e)  ... 
    

您可能不需要在单独的线程上使用它,因为您正在执行的异步工作不受 CPU 限制(或者看起来如此)。因此,您应该没问题

 try 
            var myConnectTask =cws.ConnectAsync(u, CancellationToken.None);

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // continue from here
        
        catch (Exception e)  ... 
    

它会依次执行与上面代码完全相同的操作,但在同一个线程上。它将允许“ConnectAsync”之后的代码执行,并且只会停止等待“ConnectAsync”完成,其中显示await和因为“ConnectAsync”不受 CPU 限制,所以您(在某种意义上使其在其他地方完成的工作(即网络)有点并行)将有足够的汁液来运行您的任务,除非您的代码在 "...." 还需要大量 CPU 密集型工作,您宁愿并行运行。

此外,您可能希望避免使用 async void,因为它仅适用于***函数。尝试在您的方法签名中使用 async Task。你可以阅读更多关于这个here.

【讨论】:

等等——看看你的最后一句话。这是否意味着:“...将在单独的线程上运行。然后完成后下一行代码(因此,“...”)将运行。”我说的对吗??! 这是真的,但我不建议使用它,因为如果你使用很多线程,你会看到性能下降,因为 Unitys UnitySynchronizationContext 最好使用作业老实说 async 和 await 目前是 meh (用于单独的线程) @Fattie 如果您在那里等待它,那么是的,它将等待该线程完成并完成任务。如果你想让事情并行运行,那么你应该将该任务分配给一个变量,然后在任何你想要的地方等待它。我会在我的回答中提供更多信息,但您应该能够从我发布的链接中获取该信息。 @Menyus ,好点。不过有一件事,性能很少是问题:在进行 Unity 编程时(绝对)必须知道您是否仍在 Unity 线程上。 @agfc - 等待!!!!!!!!!!!!!!!在这个惊人的系统中。在您的第一个代码示例中,它使用 Task.Run 专门创建一个新线程。最终它在“await myConnectTask”中“回来”。事实上,它知道“回来” ON THE ORIGINAL THREAD 吗?如果是这样 - 天哪,那太聪明了!!!!!!!!!【参考方案2】:

不,async/await 并不意味着 - 另一个线程。它可以启动另一个线程,但不是必须的。

在这里你可以找到相当有趣的帖子:https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

【讨论】:

啊,谢谢 - 那是来自一个真正的 MSFT 家伙,对。该死,我必须更仔细地阅读它 - 它仍然令人困惑! 它将永远启动另一个线程。 await 本身不执行任何操作,它等待已经执行的异步操作而不阻塞当前线程 @Fattie 这并不令人困惑 - await 等待,它不会启动任何东西。将async 添加到方法签名不会神奇地使其异步。 @PanagiotisKanavos - 太棒了,明白了。你应该学习Unity,你会通过真正理解c#来发财:)【参考方案3】:

重要提示

首先,您的问题的第一个陈述存在问题。

Unity 是单线程的

Unity 不是单线程的;事实上,Unity 是一个多线程环境。为什么?只需转到official Unity web page 并在那里阅读:

高性能多线程系统:充分利用当今(和未来)可用的多核处理器,无需繁重的编程。我们实现高性能的新基础由三个子系统组成:C# 作业系统,它为您提供了一个用于编写并行代码的安全且简单的沙箱;实体组件系统 (ECS),一种默认编写高性能代码的模型,以及生成高度优化的原生代码的 Burst Compiler。

Unity 3D 引擎使用名为“Mono”的 .NET 运行时,其本质上多线程的。对于某些平台,托管代码将转换为本机代码,因此不会有 .NET 运行时。但是代码本身无论如何都是多线程的。

所以请不要陈述误导性和技术上不正确的事实。

您争论的只是一个声明,Unity 中有一个主线程,它以基于帧的方式处理核心工作负载。这是真实的。但这并不是什么新奇独特的东西!例如。在 .NET Framework(或从 3.0 开始的 .NET Core)上运行的 WPF 应用程序也有一个主线程(通常称为 UI 线程),工作负载在该线程上以基于帧的方式处理使用 WPF Dispatcher(调度程序队列、操作、帧等)的方式但是所有这些都不会使环境成为单线程的!这只是处理应用程序逻辑的一种方式。


回答您的问题

请注意:我的回答仅适用于运行 .NET 运行时环境 (Mono) 的此类 Unity 实例。对于那些将托管 C# 代码转换为本机 C++ 代码并构建/运行本机二进制文件的实例,我的回答很可能至少是不准确的。

你写:

当你在另一个线程上做某事时,你经常使用 async/wait,因为,嗯,所有优秀的 C# 程序员都说这是做这件事的简单方法!

C# 中的 asyncawait 关键字只是使用 TAP (Task-Asynchronous Pattern) 的一种方式。

TAP 用于任意异步操作。一般来说,没有线程。我强烈推荐阅读this Stephen Cleary's article called "There is no thread"。 (如果您不知道,Stephen Cleary 是一位著名的异步编程大师。)

使用async/await 功能的主要原因是异步操作。您使用async/await 不是因为“您在另一个线程上做某事”,而是因为您有一个必须等​​待的异步操作。无论是否存在此操作将运行的后台线程 - 这对您来说并不重要(好吧,几乎;见下文)。 TAP 是隐藏这些细节的抽象层。

事实上,上面的代码是否真的启动了另一个线程,或者当您使用 natty async/wait 模式时,c#/.Net 是否使用其他方法来完成任务?

正确答案是:视情况而定

如果ClientWebSocket.ConnectAsync 立即抛出参数验证异常(例如,ArgumentNullExceptionuri 为空时),不会启动新线程 如果该方法中的代码很快完成,则该方法的结果将同步可用,不会启动新线程 如果ClientWebSocket.ConnectAsync 方法的实现是一个不涉及线程的纯异步操作,您的调用方法 将被“暂停”(由于await) - 所以否新话题将开始 如果方法实现涉及线程并且当前TaskScheduler能够在一个正在运行的线程池线程上调度这个工作项,则不会启动新线程;相反,工作项将在已在运行的线程池线程上排队 如果所有线程池线程都已忙,则运行时可能会根据其配置和当前系统状态生成新线程,所以是的 - 可能会启动新线程,并且工作项将排队在那个新线程上

你看,这非常复杂。但这正是将 TAP 模式和 async/await 关键字对引入 C# 的原因。这些通常是开发人员不想打扰的事情,所以让我们将这些东西隐藏在运行时/框架中。

@agfc 陈述了一个不太正确的事情:

“这不会在后台线程上运行该方法”

await cws.ConnectAsync(u, CancellationToken.None);

“但这会”

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

如果ConnectAsync 的同步部分实现很小,则任务调度程序可能在这两种情况下同步运行该部分。因此,这两个 sn-ps 可能完全相同,具体取决于调用的方法实现。

请注意,ConnectAsync 具有 Async 后缀并返回 Task。这是一个基于约定的信息,表明该方法是真正异步的。在这种情况下,您应该始终更喜欢await MethodAsync() 而不是await Task.Run(() => MethodAsync())

进一步有趣的阅读:

await vs await Task.Run return Task.Run vs await Task.Run

【讨论】:

@Fattie,我取消了我的答案。我之前删除了它,因为它只适用于那些运行 .NET 运行时的 Unity 实例。对于所有 Unity 变体,包括将 C# 代码转换为本机代码的变体,我没有通用答案。【参考方案4】:

我不喜欢回答我自己的问题,但事实证明这里没有一个答案是完全正确的。 (但是这里的许多/所有答案在不同方面都非常有用)。

其实,实际的答案可以简而言之:

await 后在哪个线程上恢复执行由SynchronizationContext.Current 控制。

就是这样。

因此,在任何特定版本的 Unity 中(请注意,截至 2019 年,它们正在剧烈改变 Unity - https://unity.com/dots) - 或者实际上是任何 C#/.Net 环境 -此页面上的问题可以正确回答。

完整的信息出现在后续的 QA 中:

https://***.com/a/55614146/294884

【讨论】:

【参考方案5】:

await 之后的代码将在另一个线程池线程上继续。这在处理方法中的非线程安全引用时可能会产生后果,例如 Unity、EF 的 DbContext 和许多其他类,包括您自己的自定义代码。

举个例子:

    [Test]
    public async Task TestAsync()
    
        using (var context = new TestDbContext())
        
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread Before Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = await names;
            Console.WriteLine("Thread After Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        
    

输出:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 29
Thread Before Await: 29
Thread After Await: 12

1 passed, 0 failed, 0 skipped, took 3.45 seconds (NUnit 3.10.1).

注意ToListAsync 之前和之后的代码在同一个线程上运行。因此,在等待任何结果之前,我们可以继续处理,尽管异步操作的结果将不可用,只有创建的 Task 可用。 (可以中止、等待等) 一旦我们将await 放入,后面的代码将被有效地拆分为一个延续,并且将/可以返回到不同的线程。

这适用于在线等待异步操作:

    [Test]
    public async Task TestAsync2()
    
        using (var context = new TestDbContext())
        
            Console.WriteLine("Thread Before Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = await context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        
    

输出:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async/Await: 6
Thread After Async/Await: 33

1 passed, 0 failed, 0 skipped, took 4.38 seconds (NUnit 3.10.1).

同样,await 之后的代码是在原来的另一个线程上执行的。

如果要确保调用异步代码的代码保持在同一个线程上,则需要使用Task 上的Result 来阻塞线程,直到异步任务完成:

    [Test]
    public void TestAsync3()
    
        using (var context = new TestDbContext())
        
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = names.Result;
            Console.WriteLine("Thread After Result: " + Thread.CurrentThread.ManagedThreadId.ToString());
        
    

输出:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 20
Thread After Async: 20
Thread After Result: 20

1 passed, 0 failed, 0 skipped, took 4.16 seconds (NUnit 3.10.1).

因此,就 Unity、EF 等而言,在这些类不是线程安全的情况下,您应该谨慎使用异步。例如以下代码可能会导致意外行为:

        using (var context = new TestDbContext())
        
            var ids = await context.Customers.Select(x => x.CustomerId).ToListAsync();
            foreach (var id in ids)
            
                var orders = await context.Orders.Where(x => x.CustomerId == id).ToListAsync();
                // do stuff with orders.
            
        

就代码而言,这看起来不错,但 DbContext 不是线程安全的,当根据等待查询订单时,单个 DbContext 引用将在不同的线程上运行初始客户负载。

当异步调用比同步调用有显着优势时使用异步,并且您确定延续只会访问线程安全代码。

【讨论】:

再次感谢 - FTR 事实上我已经写了一个相关的问题! ***.com/questions/55613081/… 您的回答不正确。 The code after an await will continue on another threadpool thread - 这是错误的。事实上,延续取决于该线程的SynchronizationContextawait 之前的代码在该处运行。例如。在 WinForms 或 WPF 应用程序中,await 之后的代码将(默认情况下)在执行await 之前的代码的同一线程上继续。在您的示例中并非如此,因为您没有SynchronizationContext。但是 Unity 在其主线程上 SynchronizationContext,因此await 之后的代码将在该线程上继续,而不是在线程池线程上。 确实如此,这对我来说是个新闻。这实际上意味着它在幕后执行与示例 #3 相同的操作,但如果您正在编写自己的异步方法,您仍然需要注意它们是在单独的线程中执行的,只需使用同步上下文即可恢复回到原来的线程。 (即保持异步方法正常运行) 谢谢!很高兴看到这种行为听起来受到了控制。【参考方案6】:

我很惊讶在这个线程中没有提到 ConfigureAwait。我正在寻找一个确认,即 Unity 执行 async/await 的方式与为“常规”C# 执行的方式相同,从我在上面看到的情况来看似乎是这样。

问题是,默认情况下,等待的任务将在完成后在相同的线程上下文中恢复。如果您在主线程上等待,它将在主线程上恢复。如果您等待线程池中的线程,它将使用线程池中的任何可用线程。您始终可以为不同的目的创建不同的上下文,例如数据库访问上下文等等。

这就是 ConfigureAwait 的有趣之处。如果您在等待之后链接到ConfigureAwait(false) 的调用,则您是在告诉运行时您不需要在相同的上下文中恢复,因此它将在线程池中的线程上恢复。省略对 ConfigureAwait 的调用会很安全,并且会在相同的上下文中恢复(主线程、数据库线程、线程池上下文,无论调用者所在的上下文是什么)。

因此,从主线程开始,您可以像这样在主线程中等待和恢复:

    // Main thread
    await WhateverTaskAync();
    // Main thread

或者像这样进入线程池:

    // Main thread
    await WhateverTaskAsync().ConfigureAwait(false);
    // Pool thread

同样,从池中的一个线程开始:

    // Pool thread
    await WhateverTaskAsync();
    // Pool thread

相当于:

    // Pool thread
    await WhateverTaskAsync().ConfigureAwait(false);
    // Pool thread

要返回主线程,您可以使用传输到主线程的 API:

    // Pool thread
    await WhateverTaskAsync().ConfigureAwait(false);
    // Pool thread
    RunOnMainThread(()
    
        // Main thread
        NextStep()
    );
    // Pool thread, possibly before NextStep() is run unless RunOnMainThread is synchronous (which it normally isn't)

这就是为什么人们说调用 Task.Run 在池线程上运行代码的原因。虽然等待是多余的......

   // Main Thread
   await Task.Run(()
   
       // Pool thread
       WhateverTaskAsync()
       // Pool thread before WhateverTaskAsync completes because it is not awaited
    );
    // Main Thread before WhateverTaskAsync completes because it is not awaited

现在,调用 ConfigureAwait(false) 并不能保证 Async 方法中的代码在单独的线程中被调用。它只说明当从等待返回时,您无法保证处于相同的线程上下文中。

如果您的 Async 方法如下所示:

private async Task WhateverTaskAsync()

    int blahblah = 0;

    for(int i = 0; i < 100000000; ++i)
    
        blahblah += i;
    

... 因为 Async 方法内部实际上没有 await,所以您会收到一个编译警告,并且它都会在调用上下文中运行。根据其 ConfigureAwait 状态,它可能会在相同或不同的上下文中恢复。如果您希望该方法在池线程上运行,则可以这样编写 Async 方法:

private Task WhateverTaskAsync()

    return Task.Run(()
    
        int blahblah = 0;

        for(int i = 0; i < 100000000; ++i)
        
            blahblah += i;
        
    

希望这可以为其他人解决一些问题。

【讨论】:

以上是关于在 Unity / C# 中,.Net 的 async/await 是不是从字面上开始另一个线程?的主要内容,如果未能解决你的问题,请参考以下文章

Unity的未来,是固守Mono,还是拥抱CoreCLR?

C#系 SQLite4Unity的介绍

错误记录Visual Studio 2019 中运行 Unity C# 脚本时报错 ( 根据解决方案, 可能需要安装额外的组件才能获得 | .NET 桌面开发 | 使用 Unity 的游戏开发 )

unity(c# ioc框架) 使用总结

Unity中的异步编程——在Unity中使用 C#原生的异步(Task,await,async) - System.Threading.Tasks

极简入门:从Unity到Asp .net core!