如何从头开始实现异步I / O绑定操作?

时间:2018-06-22 10:06:27

标签: c# asynchronous async-await

我试图了解如何以及何时使用async编程,并开始进行 I / O绑定操作,但是我不理解它们。我想从头开始。我该怎么办?

考虑下面的示例是同步的:

private void DownloadBigImage() {
    var url = "https://cosmos-magazine.imgix.net/file/spina/photo/14402/180322-Steve-Full.jpg";
    new WebClient().DownloadFile(url, "image.jpg");
}

如何仅通过使用普通的同步方法async 而不使用DownloadBigImage 来实现Task.Run版本,因为这将使用线程池中的线程只为等待-那只是浪费!

不要使用已经async 的特殊方法!这是这个问题的目的:如何在不依靠已经异步的方法的情况下自己实现?因此,之类的东西:

await new WebClient().DownloadFileTaskAsync(url, "image.jpg");

在这方面非常缺乏可用的示例和文档。我发现只有这样: https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth 上面写着:

  

对GetStringAsync()的调用通过较低级别的.NET库(可能调用其他异步方法)进行,直到到达对本机网络库的P / Invoke互操作调用。本机库随后可以调用系统API调用(例如,对Linux上的套接字的write())。可能使用TaskCompletionSource在本机/托管边界创建一个任务对象。任务对象将在各个层中向上传递,可能会对其进行操作或直接返回,最终返回给初始调用者。

基本上,我必须使用“ P /向内部网络库调用互操作” ...但是如何?

3 个答案:

答案 0 :(得分:8)

我认为这是一个非常有趣的问题,也是一个有趣的学习练习。

从根本上讲,您不能使用任何现有的同步API。一旦同步,就无法将其真正变为异步。您正确地识别出Task.Run及其等效物不是解决方案。

如果拒绝调用任何异步.NET API,则需要使用PInvoke调用本机API。这意味着您需要调用WinHTTP API或直接使用套接字。这是可能的,但我没有经验来指导您。

相反,您可以使用异步托管套接字来实现异步HTTP下载。

从同步代码开始(这是原始草图):

using (var s = new Socket(...))
{
 s.Connect(...);
 s.Send(GetHttpRequestBytes());
 var response = new StreamReader(new NetworkStream(s)).ReadToEnd();
}

这很粗略地使您获得HTTP响应作为字符串。

您可以使用await轻松地使它真正地异步。

using (var s = new Socket(...))
{
 await s.ConnectAsync(...);
 await s.SendAsync(GetHttpRequestBytes());
 var response = await new StreamReader(new NetworkStream(s)).ReadToEndAsync();
}

如果您考虑针对自己的运动目标await作弊,则需要使用回调编写代码。这太糟糕了,所以我只想编写连接部分:

var s = new Socket(...)
s.BeginConnect(..., ar => {
   //perform next steps here
}, null);

同样,此代码非常原始,但是显示了原理。您无需注册IO(在Connect内部隐式发生),而是注册一个在IO完成时调用的回调。这样,您的主线程将继续运行。这会将您的代码变成意大利面条。

您需要使用回调编写安全的处理方式。这是一个问题,因为异常处理无法跨越回调。另外,如果您不想依赖框架来执行此操作,则可能需要编写一个读取循环。异步循环可能会让人弯腰。

答案 1 :(得分:1)

这是一个很好的问题,在大多数有关C#和异步的文章中,并没有很好地解释。

我搜索了很长时间,以为我可以并且应该实现自己的异步I / O方法。如果我使用的方法/库没有异步方法,我想我应该以某种方式将这些函数包装在使它们异步的代码中。事实证明,这对于大多数程序员而言实际上并不可行。是的,您可以使用Thread.Start(() => {...})产生一个新线程,这确实使您的代码异步,但是它还会创建一个新线程,这对于异步操作来说是一笔昂贵的开销。它当然可以释放UI线程以确保您的应用程序保持响应状态,但不会像HttpClient.GetAsync()是真正的异步操作那样创建真正的异步操作。

这是因为.net库中的异步方法使用称为“ .NET中的标准P / Invoke异步I / O系统”的东西来调用底层OS代码,该代码在执行出站IO时不需要专用的CPU线程(网络或存储)。实际上,它并没有将线程专用于其工作,而是在完成工作后向.net运行时发出信号。

我对细节不熟悉,但是这种知识足以使我摆脱尝试实现异步I / O的注意力,使我专注于使用.net库中已经存在的异步方法(例如HttpClient.GetAsync( ))。可以找到更有趣的信息here (Microsoft async deep dive)和一个不错的description by Stephen Cleary here

答案 2 :(得分:-1)

创建一个执行同步代码的新任务。该任务将由线程池的线程执行。

private async Task DownloadBigImage() 
{
      await Task.Run(()=>
      {
            var url = "https://cosmos-magazine.imgix.net/file/spina/photo/14402/180322-Steve-Full.jpg";
            new WebClient().DownloadFile(url, "image.jpg");
      });
}