如何在C#中创建异步方法?

Kha*_*meh 183 c# async-await c#-5.0

我读过的每篇博文都告诉你如何在C#中使用异步方法,但由于一些奇怪的原因,从未解释如何构建自己的异步方法来使用.所以我现在有这个代码消耗我的方法:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}
Run Code Online (Sandbox Code Playgroud)

我写的这个方法是CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}
Run Code Online (Sandbox Code Playgroud)

这是使用Task.Factory,编写异步方法的最佳方法,还是应该用另一种方式编写?

Ste*_*ary 216

StartNew除非你需要那么高的复杂程度,否则我不建议.

如果您的异步方法依赖于其他异步方法,最简单的方法是使用async关键字:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}
Run Code Online (Sandbox Code Playgroud)

如果您的异步方法正在执行CPU工作,您应该使用Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}
Run Code Online (Sandbox Code Playgroud)

您可能会发现我的async/ await介绍很有帮助.

  • @Stephen:"如果您的异步方法依赖于其他异步方法" - 好的,但如果不是这样的话会怎么样.如果试图用一些回调代码包装一些调用BeginInvoke的代码怎么办? (10认同)
  • @jp2code:`await`是一个"异步等待",所以直到`Task.Delay`返回的任务完成后才会进行下一次迭代. (3认同)
  • @Ricibob:您应该使用`TaskFactory.FromAsync` 来包装`BeginInvoke`。不确定您对“回调代码”的意思;随时用代码发布您自己的问题。 (2认同)

Alb*_*rto 11

如果您不想在方法中使用async/await,但仍然"装饰"它以便能够从外部使用await关键字,TaskCompletionSource.cs:

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}
Run Code Online (Sandbox Code Playgroud)

从这里这里

为了支持使用Tasks的这种范例,我们需要一种方法来保留Taskfaçade以及将任意异步操作作为Task引用的能力,但是要根据提供的任务的底层基础结构的规则来控制该Task的生命周期.异步,并以不显着成本的方式这样做.这是TaskCompletionSource的目的.

我看到也用于.NET源代码,例如.WebClient.cs:

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }
Run Code Online (Sandbox Code Playgroud)

最后,我发现以下内容也很有用:

我一直被问到这个问题.这意味着必须有某个线程阻塞对外部资源的I/O调用.因此,异步代码释放了请求线程,但只是牺牲了系统中其他位置的另一个线程,对吧?一点都不.为了理解异步请求扩展的原因,我将跟踪异步I/O调用的(简化)示例.假设请求需要写入文件.请求线程调用异步写入方法.WriteAsync由基类库(BCL)实现,并为其异步I/O使用完成端口.因此,WriteAsync调用作为异步文件写入传递给操作系统.然后,OS与驱动程序堆栈通信,传递数据以写入I/O请求数据包(IRP).事情变得有趣:如果设备驱动程序无法立即处理IRP,它必须异步处理它.因此,驱动程序告诉磁盘开始写入并向操作系统返回"挂起"响应.操作系统将"挂起"响应传递给BCL,BCL将不完整的任务返回给请求处理代码.请求处理代码等待该任务,该任务从该方法返回不完整的任务,依此类推.最后,请求处理代码最终将一个不完整的任务返回给ASP.NET,并释放请求线程以返回到线程池.

ASP.NET上的Async/Await简介

如果目标是提高可伸缩性(而不是响应性),那么它都依赖于外部I/O的存在,这提供了实现这一目标的机会.


小智 8

使方法异步的一种非常简单的方法是使用 Task.Yield() 方法。正如 MSDN 所说:

您可以使用await Task.Yield(); 在异步方法中强制该方法异步完成。

将其插入方法的开头,然后它将立即返回调用者并在另一个线程上完成方法的其余部分。

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
Run Code Online (Sandbox Code Playgroud)