【问题标题】:Async/Await action within Task.Run()Task.Run() 中的异步/等待操作
【发布时间】:2016-01-12 23:35:54
【问题描述】:

Task.Run(()=>{}) 将操作委托放入队列并返回任务。 在Task.Run() 中使用 async/await 有什么好处吗? 我知道Task.Run()是必需的,因为如果我们想直接使用await,那么调用方法将需要异步并且会影响调用位置。

这是在Task.Run() 中具有异步等待的示例代码。此处提供完整示例:Create pre-computed tasks。

Task.Run(async () => { await new WebClient().DownloadStringTaskAsync("");});

也可以这样做:

Task.Run(() => new WebClient().DownloadStringTaskAsync("").Result;);

由于Task.Run() 和 Await 都会将工作排队并由线程池选择,Task.Run() 中的 async/await 会不会有点多余?

【问题讨论】:

    标签: c# .net asynchronous async-await


    【解决方案1】:

    在 Task.Run() 中使用 async/await 有什么好处吗?

    是的。 Task.Run 在线程池线程上运行一些操作。如果这个动作做了一些 IO 工作,并通过await 异步等待 IO 操作完成,那么这个线程池线程可以在 IO 操作仍在运行时被系统用于其他工作。

    例子:

    Task.Run( async () =>
    {
        DoSomeCPUIntensiveWork();
    
        // While asynchronously waiting for this to complete, 
        // the thread is given back to the thread-pool
        var io_result = await DoSomeIOOperation(); 
    
        DoSomeOtherCPUIntensiveWork(io_result);
    });
    

    【讨论】:

    【解决方案2】:

    在 Task.Run() 中使用 async/await 有什么好处

    async 方法在第一个 await 被命中后立即返回给调用者(对未完成的任务进行操作)。因此,如果异步方法的第一次“连续”执行需要很长时间Task.Run 将改变行为:它将导致该方法立即返回并在线程池上执行第一次“连续”。

    这在 UI 场景中很有用,因为这样您可以 100% 确保您没有阻塞 UI。示例:HttpWebRequestdoes DNS 同步解析,即使您使用其中一种异步方法(这基本上是库错误/设计错误)。这可以暂停 UI 线程。因此,您可以使用 Task.Run 来 100% 确保 UI 不会被阻塞超过几微秒。

    回到最初的问题:为什么要在 Task.Run 主体中等待?出于同样的原因,您通常等待:解除阻塞线程。

    【讨论】:

    • "为什么要在 Task.Run 中包装异步/等待代码?!"因为例如我需要在同步方法中运行异步代码,我无法更改或更改所有调用者的风险太大。那么
    • @David 在这种情况下,通常对正在返回的任务调用 Wait/Result 就足够了。
    【解决方案3】:

    从非异步方法调用异步

    当我们尝试在非异步方法中调用异步方法时,我们会做一些类似的事情。特别是如果异步方法是已知数量。虽然我们更多地使用了 TaskFactory ... 符合一种模式,使其更易于调试,确保每个人都采用相同的方法(并且 - 如果 async-->sync 开始出现错误,我们会感到窒息)。

    那么,你的例子

    想象一下,在您的示例中,您有一个非异步函数。而且,在该函数中,您需要调用 await webClient.DoSomethingAsync()。您不能在非异步函数中调用 await —— 编译器不会让您这样做。

    选项 1:僵尸感染

    您的第一个选择是一路爬取备份调用堆栈,将沿途的每个 da*n 方法标记为异步,并在各处添加等待。到处。就像,无处不在。然后 - 由于所有这些方法现在都是异步的,因此您需要使引用它们的方法全部异步。

    顺便说一句,这可能是许多 SO 爱好者要提倡的方法。因为,“阻塞线程是个坏主意。”

    所以。是的。这种“让异步成为异步”的方法意味着你的小库例程可以获取一个 json 对象,并触及 80% 的代码。谁会打电话给 CTO 让他知道?

    选项 2:与一个僵尸一起去

    或者,你可以将你的 async 封装在像你这样的函数中......

    return Task.Run(async () => {
         content = await new WebClient().DoSomethingAsync();
         cachedDownloads.TryAdd(address, content);
         return content;
    });
    

    Presto...僵尸感染已包含在单个代码段中。我将把它留给位机制来争论如何/为什么在 CPU 级别执行。我真的不在乎。我关心没有人必须向 CTO 解释为什么整个库现在应该是 100% 异步的(或类似的东西)。

    【讨论】:

      【解决方案4】:

      在您链接的示例中,主线程被阻塞,直到异步操作完成。调用Wait() 会阻止它(顺便说一句,这通常是个坏主意)。

      让我们看一下linked 示例中DownloadStringAsync 的返回值:

      return Task.Run(async () =>
      {
          content = await new WebClient().DownloadStringTaskAsync(address);
          cachedDownloads.TryAdd(address, content);
          return content;
      });
      

      为什么要将它包装在 Task 中?考虑一下您的选择。如果您不想将其包装在Task 中,您将如何确保该方法返回Task<string> 并且仍然可以工作?您当然会将方法标记为async!但是,如果您将方法标记为 async 并在其上调用 Wait,您很可能会陷入死锁,因为主线程正在等待工作完成,并且您阻塞了主线程,所以它不能让你知道它已经完成了。

      将方法标记为async 时,状态机将在调用线程上运行,但是在您的示例中,状态机在单独的线程上运行,这意味着在主线程上几乎没有工作。

      【讨论】:

        【解决方案5】:

        确认,用Task.Run 包裹await 使用2 个线程而不是1 个。

        Task.Run(async () => { //thread #1
         await new WebClient().DownloadStringTaskAsync(""); //thread #2
        });
        

        假设你有四个这样包装的调用,它将使用 4 x 2 = 8 个线程。

        最好用简单的await 来调用它们。例如:

        Task<byte[]> t1 = new WebClient().DownloadStringTaskAsync("");
        Task<byte[]> t2 = new WebClient().DownloadStringTaskAsync("");
        byte[] t1Result = await t1;
        byte[] t2Result = await t2;
        

        这是包装 Task.Run 使用额外线程的证明。 (没有用WebClient来证明这一点)

        private static async Task wrapped()
        {
            List<Task> tasks = new List<Task>();
            tasks.AddRange(new []
            {
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
                Task.Run(async() => await new MyThread().RunMe()),
            });
        
            Thread.Sleep(1000);
            int number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"While running thread count: {number}");
        
            await Task.WhenAll(tasks);
        }
        

        解开

        private static async Task unwrapped()
        {
            List<Task> tasks = new List<Task>();
            Task<int> t1 = new MyThread().RunMe();
            Task<int> t2 = new MyThread().RunMe();
            Task<int> t3 = new MyThread().RunMe();
            Task<int> t4 = new MyThread().RunMe();
            tasks.AddRange(new[] {t1, t2, t3, t4});
        
            Thread.Sleep(1000);
            int number = Process.GetCurrentProcess().Threads.Count;
            Console.WriteLine($"While running thread count: {number}");
        
            int i1 = await t1;
            int i2 = await t2;
            int i3 = await t3;
            int i4 = await t4;
        }
        

        这里有完整的 POC 代码

        using System;
        using System.Collections.Generic;
        using System.Diagnostics;
        using System.Threading;
        using System.Threading.Tasks;
        
        namespace AsyncThreadDemo
        {
            class Program
            {
                static async Task Main(string[] args)
                {
                    int number = Process.GetCurrentProcess().Threads.Count;
                    Console.WriteLine($"Init thread count: {number}");
        
        
                    //await wrapped();
                    await unwrapped();
        
                    number = Process.GetCurrentProcess().Threads.Count;
                    Console.WriteLine($"Done thread count: {number}");
        
                    Console.ReadLine();
                }
        
                private static async Task wrapped()
                {
                    List<Task> tasks = new List<Task>();
                    tasks.AddRange(new []
                    {
                        Task.Run(async() => await new MyThread().RunMe()),
                        Task.Run(async() => await new MyThread().RunMe()),
                        Task.Run(async() => await new MyThread().RunMe()),
                        Task.Run(async() => await new MyThread().RunMe()),
                    });
        
                    Thread.Sleep(1000);
                    int number = Process.GetCurrentProcess().Threads.Count;
                    Console.WriteLine($"While running thread count: {number}");
        
                    await Task.WhenAll(tasks);
                }
        
                private static async Task unwrapped()
                {
                    List<Task> tasks = new List<Task>();
                    Task<int> t1 = new MyThread().RunMe();
                    Task<int> t2 = new MyThread().RunMe();
                    Task<int> t3 = new MyThread().RunMe();
                    Task<int> t4 = new MyThread().RunMe();
                    tasks.AddRange(new[] {t1, t2, t3, t4});
        
                    Thread.Sleep(1000);
                    int number = Process.GetCurrentProcess().Threads.Count;
                    Console.WriteLine($"While running thread count: {number}");
        
                    int i1 = await t1;
                    int i2 = await t2;
                    int i3 = await t3;
                    int i4 = await t4;
                }
            }
        
            public class MyThread
            {
                public static int _counter;
        
                public async Task<int> RunMe()
                {
                    await Task.Run(() =>
                    {
                        for (int i = 0; i < 2; ++i)
                        {
                            Thread.Sleep(1000);
                            Console.WriteLine($"T{Thread.CurrentThread.ManagedThreadId} {i}");
                        }
                        Console.WriteLine($"T{Thread.CurrentThread.ManagedThreadId} done");
                    });
                    return _counter++;
                }
            }
        }
        

        【讨论】:

        • Jeson 是的,关于 StackOverflow 规则的答案现在还可以,但从本质上讲,这不是一个很好的答案。 Task.Run 方法不会产生新线程,它会重用 ThreadPool 线程。而WebClient.DownloadStringTaskAsync 大部分使用zero threads。 ThreadPool 的使用时间很短,以至于单个线程可能能够完成所有需要的工作。
        • @TheodorZoulias 字 spawn 重命名为 use。还添加了 POC。希望这能证明这一点。将异步调用包装在 Task.Run 中会导致不必要的开销。
        • 我无法重现您的概念证明程序的结果。在我的电脑中,它打印Done thread count: 14,带有await wrapped(); 或await unwrapped();。我得到了类似的输出on Fiddle。为了完整起见,我添加了 ThreadPool.ThreadCount 的日志记录。顺便说一句,您的 PoC 与您原来的 WebClient 示例不同。它不是调用真正的异步操作,而是使用Thread.Sleep(1000); 阻塞一个线程。使用 await Task.Delay(1000); 保持 PoC 的相关性 (try it on Fiddle)。
        • 看起来这个问题是特定于 .NET 版本的。我有 .NET 框架 4.7.2、VS2019、Windows 机器中的 PoC。跑起来没问题。 .NET Fiddle 但是,在 .NET framework 4.7.2 中,调用 Proess.GetCurrentProcess 时会引发站点安全错误。尝试在本地机器上运行 4.7.2 下的 PoC 代码。 dotnetfiddle.net/plS0ka 同样公平地说,在 MyThread 类中,使用 Thread.Sleep(1000) 而不是 await Task.Delay(1000)。后者效率更高,因为它在等待时释放线程。前者拿着它。
        • Jeson 您的 PoC 观察到的是 ThreadPool 类的行为,该类可能已从 .NET Framework 更改为 .NET Core。它的行为还取决于 CPU 内核的数量,我猜你的机器有超过 4 个。仅比较不同时刻的线程数量并不能说明程序到目前为止消耗的总处理能力。坚持用Task.Run(() =&gt; Thread.Sleep()) 模拟WebClient.DownloadStringTaskAsync 会使您的答案与问题无关。前一种方法不会让线程进入睡眠状态。
        猜你喜欢
        • 1970-01-01
        • 2012-11-12
        • 1970-01-01
        • 2015-09-28
        • 2018-06-03
        • 1970-01-01
        • 2016-12-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多