在Unity / C#中,.Net的async / await是否确实从另一个线程开始?

时间:2019-03-29 13:04:51

标签: c# .net unity3d

重要,对于专门在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或其他方法,一个本机插件或OS或其他任何情况)。

一切都很好。

作为一个la脚的Unity程序员,他只知道足够的C#才能结束一天,我一直假设,上面的异步/等待模式实际上会启动另一个线程 >。

实际上,上面的代码是从字面上启动了另一个线程,还是c#/。Net在使用自然异步/等待模式时是否使用其他方法来完成任务?

它在Unity引擎中的工作方式可能与“一般使用C#”有所不同或不同? (IDK?)

请注意,在Unity中,它是否是线程会严重影响您处理后续步骤的方式。因此是问题。


问题:我意识到关于“正在等待线程”的讨论很多,但是(1)我从未见过在Unity设置中对此问题进行过讨论/回答 (这有什么区别吗?IDK吗?)(2)我只是从未见过明确的答案!


1 在其他线程上进行了一些辅助计算(例如,物理等),但是实际的“基于框架的游戏引擎”是一个纯线程。 (无论如何都无法“访问”主机框架线程:例如,在对本机插件进行编程或在另一个线程上进行某种计算时,您只需在引擎框架线程上留下组件的标记和值即可查看和在运行每帧时使用。)

6 个答案:

答案 0 :(得分:16)

这段阅读: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 ,因为它仅用于顶级功能。尝试在方法签名中使用异步任务。您可以在此here.

上阅读更多内容

答案 1 :(得分:5)

否,异步/等待并不意味着-另一个线程。它可以启动另一个线程,但不是必须的。

在这里您可以找到有关它的有趣文章:https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

答案 2 :(得分:4)

重要通知

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

  

Unity是单线程的

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

  

高性能多线程系统:无需过多编程即可充分利用当前(和将来)可用的多核处理器。我们实现高性能的新基础由三个子系统组成:C#作业系统,它为您提供了一个安全轻松的沙箱来编写并行代码;实体组件系统(ECS)和Burst编译器,后者是默认情况下用于编写高性能代码的模型,后者可生成高度优化的本机代码。

Unity 3D引擎使用一个称为“ Mono”的.NET运行时,该运行时 本质上是多线程的。对于某些平台,托管代码将转换为本机代码,因此将没有.NET Runtime。但是代码本身还是会是多线程的。

因此,请不要陈述具有误导性和技术上不正确的事实。

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


您问题的答案

请注意:我的回答仅适用于运行.NET Runtime环境(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是隐藏这些细节的抽象级别。

  

实际上,上面的代码是从字面上启动了另一个线程,还是c#/。Net在使用自然异步/等待模式时是否使用其他方法来完成任务?

正确答案是:这取决于

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

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

@agfc指出了一件不太正确的事情:

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

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

“但这会”

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

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

请注意,ConnectAsync后缀为 Async ,并返回Task。这是基于约定的信息,该方法是真正异步的。在这种情况下,与await MethodAsync()相比,您应该始终首选await Task.Run(() => MethodAsync())

更多有趣的读物:

答案 3 :(得分:1)

等待后的代码将在另一个线程池线程上继续。在诸如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).

再次,等待之后的代码在原始线程的另一个线程上执行。

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

    [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是不是线程安全的,当基于await来查询Orders时,单个DbContext引用将在不同的线程上运行。初始客户负载。

在异步调用比异步调用有显着优势的情况下,请使用异步,并且您确定继续操作将仅访问线程安全代码。

答案 4 :(得分:1)

我不喜欢回答自己的问题,但事实证明,这里没有一个答案是完全正确的。

(但是,这里的许多/所有答案在不同方面都非常有用,因此我向他们发送了丰厚的奖励。)

实际上,实际答案可以概括地说:

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

就是这样。

因此,在任何特定版本的Unity中(请注意,自编写2019以来,它们正在彻底更改Unity-https://unity.com/dots)-甚至是所有C#/。Net环境-此页面上的问题可以正确回答。

完整的信息来自后续的质量检查:

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

答案 5 :(得分:-1)

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

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

这就是 ConfigureAwait 有趣的地方。如果在 await 之后将调用链接到 ConfigureAwait(false),则是在告诉运行时您不需要在同一上下文中恢复,因此它将在来自 ThreadPool 的线程上恢复。省略对 ConfigureAwait 的调用可以确保安全,并将在相同的上下文(主线程、DB 线程、ThreadPool 上下文,无论调用者所在的上下文)中恢复。

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

    // 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;
        }
    }
}

希望这可以为其他人清理一些事情。