编写自己的async方法

本文关键字:方法 async 自己的 | 更新日期: 2023-09-27 18:14:53

我想知道如何写自己的异步方法的"正确"的方式。

我看过很多解释async/await模式的帖子,像这样:

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方法(如

)都非常有效。
  • 系统。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()中,这是所有需要的吗?如果这就是它所需要的,我需要做的唯一一件事就是为我的库中的每个方法提供async方法,如下所示:

public class FooMagic
{
    public void DoSomeMagic()
    {
        //Do some synchron magic...
    }
    public Task DoSomeMagicAsync()
    {
        //Do some async magic... ?!?
        return Task.Run(() => { DoSomeMagic(); });
    }
}
如果你能给我解释一下就好了,因为即使是这样一个高投票的问题:如何编写简单的异步方法?只是用已经存在的方法来解释它,只是使用asynn/await模式,就像上面提到的问题的评论一样:如何编写简单的异步方法?

编写自己的async方法

实际回答

你可以使用TaskCompletionSource,它有一个承诺任务,不执行任何代码,只有:

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

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

下面是Stephen Toub的AsyncManualResetEvent实现中所有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. Offloading:当你有CPU密集的工作时,你可以异步调用它,这将工作从一个线程转移到另一个线程(主要用于GUI线程)。

所以大多数。Net框架的异步调用支持async开箱即用,卸载时使用Task.Run(如示例中所示)。当您创建一个新的异步调用(例如I/O或异步同步构造)时,您实际上需要来自己实现async的唯一情况是。

这些情况是非常罕见的,这就是为什么你找到的答案大多是

"仅使用async/await模式并使用已有方法来解释它"


你可以深入了解TaskCompletionSource的性质

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

首先,我们需要理解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操作的类中看到它们的原因,例如硬盘驱动器调用,网络调用等。

我建议阅读并行PFX团队文章我应该为同步方法公开异步包装器吗?它确切地说明了你要做什么,以及为什么不建议这么做。

TL;DR:

Task.Run()是你想要的,但要小心隐藏它在你的库

我可能是错的,但是你可能正在寻找如何让cpu绑定的代码异步运行的指导[Stephen Cleary]。我也很难找到这个,我认为它如此困难的原因是这不是你应该为图书馆做的- kinda…

链接的文章是一个很好的阅读(5-15分钟,取决于),其中详细介绍了如何以及为什么使用Task.Run()作为API的一部分,而不是使用它来不阻塞UI线程-并区分了两种"类型"的长时间运行的进程,人们喜欢异步运行:

  • cpu绑定进程(一个正在处理/实际工作,需要一个单独的线程来完成它的工作)
  • 真正的异步操作(有时称为io绑定-在操作之间做一些事情,并且在操作之间有一堆等待时间,并且最好不要占用线程,而它坐在那里什么都不做)。

这篇文章涉及到API函数在各种上下文中的使用,并解释了相关的体系结构是"偏爱"同步方法还是异步方法,以及具有同步和异步方法签名的API在开发人员看来是怎样的。

最后一节"好了,关于错误的解决方案够了吗?我们如何以正确的方式解决这个问题??"进入我认为你问的问题,以这个结尾:

结论:不要使用Task。运行中实现的方法;相反,使用Task。

基本上,Task.Run()占用了一个线程,因此是用于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函数中,因为基本上每个-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())。