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

223

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

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

我写了这个方法,它叫做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);
}

使用Task.Factory编写异步方法是最好的方式吗?还是我应该采用其他方式?


26
我正在询问如何构建一个方法的一般性问题。我只想知道如何开始将我已经编写的同步方法转换为异步方法。 - Khalid Abuhakmeh
2
好的,那么一个典型的同步方法会做什么,为什么你想要将它变成异步的呢? - Eric Lippert
假设我需要批处理一堆文件并返回一个结果对象。 - Khalid Abuhakmeh
1
好的,所以:(1)高延迟操作是什么:获取文件--因为网络可能很慢或其他原因--还是进行处理--因为它需要大量CPU资源。 (2)您仍然没有说出为什么首先要使其异步。是否有UI线程您不想阻塞,或者其他原因? - Eric Lippert
28
@EricLippert,提问者给出的例子非常简单,真的不需要那么复杂。 - David B.
同意David的观点。@EricLipper,我真的觉得这是一个非常完美的问题,提供了合理的细节。 - Harshan
3个回答

263

除非你需要那种复杂度级别,否则我不建议使用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;
}
如果您的异步方法在执行 CPU 工作,应该使用 Task.Run
private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

你可能会发现我关于async/await的介绍有帮助


18
@Stephen:如果你的异步方法依赖于其他异步方法 - 好的,但如果不是这种情况呢?如果我们试图包装一些调用BeginInvoke的代码,并附加一些回调代码呢? - Ricibob
2
@Ricibob:你应该使用TaskFactory.FromAsync来包装BeginInvoke。不确定你所说的“回调代码”的意思;可以随时发布您自己的带有代码的问题。 - Stephen Cleary
1
在 for 循环中,Stephen,下一次迭代是立即调用还是在 Task.Delay 返回后调用? - user153923
3
@jp2code: await 是一种“异步等待”,因此它在 Task.Delay 返回的任务完成之前不会继续进行下一次迭代。 - Stephen Cleary
@StephenCleary 很好的回答,博客也很棒。+10 :) - Ian
显示剩余2条评论

16

如果您不想在方法内使用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; 
}

从这里这里开始

为了支持这样的任务范式,我们需要一种方式来保留任务外观和引用任意异步操作作为任务的能力,但根据提供异步性的基础结构的规则控制该任务的生命周期,并以不显著的方式完成。这就是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;
    }

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

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

ASP.NET上Async/Await的介绍

如果目标是提高可扩展性(而不是响应能力),这完全依赖于存在一个外部I/O,提供了这样的机会。


1
如果你看一下 Task.Run 实现 上面的注释,例如 _将指定的工作排队在线程池上运行,并返回该工作的任务句柄_。你实际上也是这样做的。我相信 Task.Run 会更好,因为它内部管理 CancelationToken 并对调度和运行选项进行了一些优化。 - unsafePtr
那么你使用 ThreadPool.QueueUserWorkItem 做的事情可以用 Task.Run 来完成,对吧? - Razvan
最佳实践是使用Task.Run而不是通过ThreadPool自己实现。此外,ThreadPool是一种老派的方法,因此需要使用TaskCompletionSource<T>。 - RashadRivera

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

1
在WinForms应用程序中,方法的其余部分将在同一线程(UI线程)中完成。 Task.Yield()确实很有用,对于那些希望确保返回的Task不会在创建时立即完成的情况。 - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接