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

Posted

技术标签:

【中文标题】如何在 C# 中创建异步方法?【英文标题】:How do you create an asynchronous method in C#? 【发布时间】:2013-04-10 10:06:00 【问题描述】:

我读过的每篇博文都告诉您如何在 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 的使用,是写异步方法的最佳方式,还是我应该用另一种方式来写?

【问题讨论】:

我在问一个关于如何构建方法的一般问题。我只想知道从哪里开始将我已经同步的方法变成异步方法。 好的,那么典型的同步方法做了什么为什么要让它异步 假设我必须批量处理一堆文件并返回一个结果对象。 好的,所以:(1)什么是高延迟操作:获取文件——因为网络可能很慢,或者其他——或者进行处理——因为它是 CPU 密集型的, 说。并且(2)你还没有说为什么你希望它首先是异步的。是否有您不想阻止的 UI 线程,还是什么? @EricLippert op给出的例子很基础,其实没必要那么复杂。 【参考方案1】:

我不推荐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 intro 很有帮助。

【讨论】:

@Stephen:“如果您的异步方法依赖于其他异步方法” - 好的,但如果不是这种情况怎么办。如果试图用一些回调代码包装一些调用 BeginInvoke 的代码怎么办? @Ricibob:您应该使用TaskFactory.FromAsync 包装BeginInvoke。不确定“回调代码”是什么意思;随时用代码发布您自己的问题。 Stephen,在for循环中,下一次迭代是立即调用还是在Task.Delay返回之后调用? @jp2code:await是“异步等待”,所以直到Task.Delay返回的任务完成后才会进行下一次迭代。 @StephenCleary 答案不错,博客也不错。 +10 :)【参考方案2】:

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

From here 和 here

为了支持这样的 Task 范式,我们需要一种方法来保留 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,请求线程被释放以返回线程池。

Introduction to Async/Await on ASP.NET

如果目标是提高可扩展性(而不是响应能力),这一切都依赖于提供实现此目标的机会的外部 I/O。

【讨论】:

如果您会看到Task.Run implementation 上方的评论,例如将指定的工作排入队列以在 ThreadPool 上运行并返回该工作的任务句柄。你实际上也是这样做的。我确信 Task.Run 会更好,因为它在内部管理 CancelationToken 并通过调度和运行选项进行一些优化。 那么你用 ThreadPool.QueueUserWorkItem 做的事情可以用 Task.Run 来完成,对吧? 最好的做法是使用 Task.Run 而不是通过 ThreadPool “自己动手”。此外,ThreadPool 是一种老派的方法。因此需要使用 TaskCompletionSource.【参考方案3】:

使方法异步的一种非常简单的方法是使用 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 在创建时不会立即完成的情况。

以上是关于如何在 C# 中创建异步方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中创建自定义属性

如何在 C# 中创建 Linq AND 表达式? [复制]

如何在 C# 中创建/编辑 PNG 文件?

如何在 C# 中创建一个没有密码的另一个用户的进程?

如何在 C# 中创建自定义登录 Windows 屏幕?

如何在 C# 中创建动态 Web api 客户端?