【问题标题】:Write your own async method编写自己的异步方法
【发布时间】:2014-09-17 04:40:29
【问题描述】:

我想知道如何以“正确”的方式编写自己的异步方法。

我看过很多这样解释异步/等待模式的帖子:

http://msdn.microsoft.com/en-us/library/hh191443.aspx

// Three things to note in the signature: 
//  - The method has an async modifier.  
//  - The return type is Task or Task<T>. (See "Return Types" section.)
//    Here, it is Task<int> because the return statement returns an integer. 
//  - The method name ends in "Async."
async Task<int> AccessTheWebAsync()
{ 
    // You need to add a reference to System.Net.Http to declare client.
    HttpClient client = new HttpClient();

    // GetStringAsync returns a Task<string>. That means that when you await the 
    // task you'll get a string (urlContents).
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    // You can do work here that doesn't rely on the string from GetStringAsync.
    DoIndependentWork();

    // The await operator suspends AccessTheWebAsync. 
    //  - AccessTheWebAsync can't continue until getStringTask is complete. 
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync. 
    //  - Control resumes here when getStringTask is complete.  
    //  - The await operator then retrieves the string result from getStringTask. 
    string urlContents = await getStringTask;

    // The return statement specifies an integer result. 
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value. 
    return urlContents.Length;
}

private void DoIndependentWork()
{
    resultsTextBox.Text += "Working........\r\n";
}

这适用于任何已经实现此功能的 .NET 方法,例如

  • System.IO 操作
  • 数据库操作
  • 网络相关操作(下载、上传...)

但是,如果我想编写我自己的方法,该方法需要相当长的时间才能完成,而我没有可以使用的方法,并且上面示例的 DoIndependentWork 方法中的负载很重,该怎么办?

我可以这样做:

  • 字符串操作
  • 计算
  • 处理我自己的对象
  • 聚合、比较、过滤、分组、处理资料
  • 列出操作、添加、删除、应对

我又一次偶然发现了许多帖子,人们只是执行以下操作(再次以上面的示例为例):

async Task<int> AccessTheWebAsync()
{ 
    HttpClient client = new HttpClient();

    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

    await DoIndependentWork();

    string urlContents = await getStringTask;

    return urlContents.Length;
}

private Task DoIndependentWork()
{
    return Task.Run(() => {

        //String manipulations
        //Calculations
        //Handling my own objects
        //Aggregating, comparing, filtering, grouping, handling stuff
        //List operations, adding, removing, coping
    });
}

您可能会注意到更改是DoIndependentWork 现在返回一个任务,而在AccessTheWebAsync 任务中,该方法得到了一个await

重负载操作现在被封装在Task.Run() 中,这就是全部吗? 如果这就是我需要做的唯一事情,我需要为我的库中的每个方法提供异步方法,如下:

public class FooMagic
{
    public void DoSomeMagic()
    {
        //Do some synchron magic...
    }

    public Task DoSomeMagicAsync()
    {
        //Do some async magic... ?!?
        return Task.Run(() => { DoSomeMagic(); });
    }
}

如果您能向我解释一下,那就太好了,因为即使是这样一个高票数的问题: How to write simple async method? 仅使用现有方法对其进行解释,并且仅使用 asyn/await 模式,如上述问题的评论将其引向重点: How to write simple async method?

【问题讨论】:

  • 我可能有点跑题了,但resultsTextBox.Text += "Working........\r\n"; 还能用吗?这在无 GUI 线程中运行,因此您需要调用,对吗?谢谢。
  • @Kimmax 不确定 - 仍在努力解决这个问题,但我目前的印象是async 签名会在幕后添加编组,或者可能只有当你 await 某事?
  • @CodeJockey 感谢您从死里复活这个评论:P 同时我确实了解更多并且知道使用类似 Progress.Report 和数据绑定的东西可以完全解决这个问题。
  • 好问题,我一直想知道完全相同的事情,但没有找到任何好的例子。最佳答案有助于澄清一些事情,但就像您一样,我希望从该答案中获得更多信息。

标签: c# .net asynchronous task-parallel-library async-await


【解决方案1】:

实际答案

您可以使用 TaskCompletionSource 来执行此操作,它有一个 Promise Task 不执行任何代码,并且仅:

“表示未绑定到委托的 Task 的生产者端,通过 Task 属性提供对消费者端的访问。”

您在启动异步操作时将该任务返回给调用者,并在结束它时设置结果(或异常/取消)。确保操作真的是异步的就靠你了。

这是Stephen Toub's AsyncManualResetEvent 实现中这种 root of all async 方法的一个很好的例子:

class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return _tcs.Task; } 
    public void Set() { _tcs.TrySetResult(true); } 
    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = _tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref _tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}

背景

使用async-await基本上有两个原因:

  1. 改进的可扩展性:当您有I/O 密集工作(或其他固有的异步操作)时,您可以异步调用它,这样您就可以释放调用线程,同时它能够执行其他工作。
  2. 卸载:当您有 CPU 密集的工作时,您可以异步调用它,这会将工作从一个线程转移到另一个线程(主要用于 GUI 线程)。

因此,大多数 .Net 框架的异步调用都支持开箱即用的async,并且您使用Task.Run 进行卸载(如您的示例中所示)。您真正需要自己实现async 的唯一情况是创建新的异步调用(例如I/O 或异步synchronization constructs)。

这些情况极为罕见,这就是为什么您大多会找到这样的答案

“只用已有的方法解释,只用async/await模式”


你可以深入The Nature of TaskCompletionSource

【讨论】:

    【解决方案2】:

    如果你能给我解释一下就好了:如何编写简单的异步 方法?

    首先,我们需要了解async 方法的含义。当向使用async 方法的最终用户公开async 方法时,您是在告诉他:“听着,此方法将很快返回给您,并承诺在不远的将来”。这就是您向用户保证的内容。

    现在,我们需要了解Task 如何使这个“承诺”成为可能。正如您在问题中所问的那样,为什么只需在我的方法中添加 Task.Run 就可以使使用 await 关键字等待有效?

    Task 实现了GetAwaiter 模式,这意味着它返回一个称为awaiter 的对象(它实际上称为TaskAwaiter)。 TaskAwaiter 对象实现INotifyCompletionICriticalNotifyCompletion 接口,公开OnCompleted 方法。

    一旦使用了await 关键字,编译器就会依次使用所有这些好东西。编译器将确保在设计时,您的对象实现GetAwaiter,然后使用它来将代码编译为状态机,这将使您的程序在等待时将控制权交还给调用者,并在等待时恢复工作已经完成。

    现在,有一些准则需要遵循。真正的异步方法不会在幕后使用额外的线程来完成其工作(Stephan Cleary 在There Is No Thread 中精彩地解释了这一点),这意味着公开使用Task.Run 内部的方法对您的api 的使用者有点误导,因为他们将假定您的任务中不涉及额外的线程。您应该做的是同步公开您的 API,并让用户使用Task.Run 自己卸载它,控制执行流程。

    async 方法主要用于 I/O Bound 操作,因为这些操作在执行 IO 操作时自然不需要消耗任何线程,这就是我们看到它们的原因很多类中负责做IO操作,比如硬盘调用、网络调用等。

    我建议阅读 Parallel PFX 团队文章 Should I expose asynchronous wrappers for synchronous methods?,该文章准确地说明了您正在尝试做什么以及为什么不建议这样做。

    【讨论】:

    • 我对答案并不满意,因为我错过了答案中的“你是如何写的”我只得到“如何不写”,也许你可以提供一些代码示例你将如何编写一个简单的异步方法?我更关注的是卸载而不是可扩展性,因为我的项目是一个实体框架项目,而 EF 它本身不支持具有 1 个上下文的多异步操作。
    • 我认为这是在澄清你的问题,因为异步的定义可能很模糊。 Yuval 说,从广义上讲,您 编写了一个异步方法。但是,从严格意义上讲,它不是真正的异步方法,因为您将工作交给了下面的任务(或线程);在严格的异步方法中没有线程——操作系统将把责任交给一些硬件。
    • 一个很棒的 MSDN article 在这个主题上。
    • @YuvalItzchakov 是否有关于如何编写真正的异步方法的代码示例(没有额外的线程并且不等待其他异步方法)?
    【解决方案3】:

    TL;DR:

    Task.Run() 是您想要的,但要小心将其隐藏在您的库中。

    我可能是错的,但您可能正在寻找有关 getting CPU-Bound code to run asynchronously 的指导 [by Stephen Cleary]。我也很难找到这个,我认为它如此困难的原因是这有点不是你应该为图书馆做的 - 有点......

    文章

    链接的文章很好读(5-15 分钟,视情况而定),其中详细介绍了使用 Task.Run() 作为 API 的一部分与使用它来不阻塞 UI 线程的方式和原因- 并区分人们喜欢异步运行的两种“类型”的长时间运行的进程:

    • 受 CPU 限制的进程(一个正在运行/实际工作的进程,需要一个单独的线程来完成它的工作)
    • 真正的异步操作(有时称为 IO 绑定 - 一种在操作之间做一些事情的地方和那里有一堆等待时间,最好不要在坐在那里做的时候占用一个线程没有)。

    本文涉及 API 函数在各种上下文中的使用,并解释了相关架构“更喜欢”同步方法还是异步方法,以及具有同步和异步方法签名的 API 在开发人员眼中的“外观”。

    回答

    最后一部分“好的,关于错误的解决方案已经够多了?我们如何以正确的方式解决这个问题???”进入了我认为您要问的内容,以以下结尾:

    结论:不要在方法的实现中使用Task.Run;而是使用 Task.Run 调用该方法。

    基本上,Task.Run() 'hogs' 一个线程,因此是用于 CPU 密集型工作的东西,但它归结为 在哪里 使用它。当您尝试做一些需要大量工作且不想阻塞 UI 线程的事情时,请使用Task.Run() 来运行辛勤工作函数直接(即在事件处理程序或基于 UI 的代码):

    class MyService
    {
      public int CalculateMandelbrot()
      {
        // Tons of work to do in here!
        for (int i = 0; i != 10000000; ++i)
          ;
        return 42;
      }
    }
    
    ...
    
    private async void MyButton_Click(object sender, EventArgs e)
    {
      await Task.Run(() => myService.CalculateMandelbrot());
    }
    

    但是...不要将你的Task.Run()隐藏在一个后缀为-Async的API函数中,如果它是一个CPU绑定函数,因为基本上每个-Async函数都是真正异步的,并且不受 CPU 限制。

    // Warning: bad code!
    class MyService
    {
      public int CalculateMandelbrot()
      {
        // Tons of work to do in here!
        for (int i = 0; i != 10000000; ++i)
          ;
        return 42;
      }
    
      public Task<int> CalculateMandelbrotAsync()
      {
        return Task.Run(() => CalculateMandelbrot());
      }
    }
    

    换句话说,不要调用 CPU 绑定函数 -Async,因为用户会认为它是 IO 绑定的 - 只需使用 Task.Run() 异步调用它,并让其他用户在他们觉得它的时候也这样做合适的。或者,将其命名为对您有意义的其他名称(可能是 BeginAsyncIndependentWork()StartIndependentWorkTask())。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多