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


196

我读过的每篇博客文章都告诉您如何使用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,是编写异步方法的最佳方法,还是应该以其他方式编写?


22
我在问一个有关如何构造方法的一般性问题。我只想知道从什么地方开始,将已经同步的方法转变为异步的方法。
卡利德·阿布哈克

2
好的,那么典型的同步方法有什么作用为什么要使其异步
埃里克·利珀特

假设我必须批处理一堆文件并返回结果对象。
Khalid Abuhakmeh 2013年

1
好的,所以:(1)什么是高延迟操作:例如,获取文件-因为网络可能很慢,或者执行处理-因为它占用大量CPU。并且(2)您仍然没有说过为什么首先要使其异步。是否有您不想阻塞的UI线程?
埃里克·利珀特

21
@EricLippert op给出的示例非常基本,实际上并不需要那么复杂。
David B.

Answers:


226

我不建议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介绍很有帮助。


10
@Stephen:“如果您的异步方法依赖于其他异步方法”-好的,但是如果不是这样的话。如果试图包装一些使用一些回调代码调用BeginInvoke的代码怎么办?
里奇博布2013年

1
@Ricibob:您应该使用TaskFactory.FromAsyncwrap BeginInvoke。不确定您对“回调代码”的含义;随时用代码发布您自己的问题。
斯蒂芬·克利西

@Stephen:谢谢-是的,TaskFactory.FromAsync是我想要的。
里奇博布

1
Stephen在for循环中,下一次迭代是立即调用还是在Task.Delay返回之后调用?
jp2code 2015年

3
@ jp2code:await是一个“异步等待”,因此它直到进行的返回的任务Task.Delay完成后才继续进行下一个迭代。
Stephen Cleary 2015年

11

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

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

从这里这里

为了用Tasks支持这种范例,我们需要一种方法来保留Task的外观以及将任意异步操作称为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;
    }

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

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

ASP.NET上的Async / Await简介

如果目标是提高可伸缩性(而不是响应性),则全部依靠外部I / O的存在来提供实现此目标的机会。


1
如果您将在Task.Run实现上方看到注释,例如,将指定的工作排队在ThreadPool上运行,并返回该工作的Task句柄。您实际上也是如此。我确信Task.Run会更好,因为它可以在内部管理CancellationToken并通过计划和运行选项进行一些优化。
unsafePtr

因此您可以通过Task.Run完成对ThreadPool.QueueUserWorkItem的操作,对吗?
拉兹万

-1

使方法异步的一种非常简单的方法是使用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;
}

在WinForms应用程序中,其余方法将在同一线程(UI线程)中完成。对于Task.Yield()确实要确保返回的Task内容在创建后不会立即完成的情况,确实有用。
Theodor Zoulias
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.