13

我最近刚刚遇到 IAsyncResult 并且已经使用了很长一段时间。我真正想知道的是,当我们在那里有更好的替代 ThreadPool 时,为什么要使用 IAsyncResult ?根据我目前对它们的理解,我会选择在几乎所有情况下使用 ThreadPool。所以我的问题是,在任何情况下 IAsyncResult 比另一个更受欢迎吗?

为什么我不喜欢 IAsyncResult:

  • BeginXXX 和 EndXXX 增加了复杂性
  • 如果调用者不关心返回值,调用者可能会忘记调用 EndXXX
  • API 设计中增加的冗余(我们需要为我们想要异步运行的每个方法创建 Begin 和 End 包装器方法)
  • 降低可读性

把它放在代码中:

线程池

  public void ThreadPoolApproach()
  {
     ThreadPool.QueueUserWorkItem( ( a ) =>
     {
        WebClient wc = new WebClient();
        var response = wc.DownloadString( "http://www.test.com" );
        Console.WriteLine( response );
     } );
  }

IAsyncResult

  public void IAsyncResultApproach()
  {
     var a = BeginReadFromWeb( ( result ) =>
     {
        var response = EndReadFromWeb( result );
        Console.WriteLine( response );
     }, "http://www.test.com" );
  }

  public IAsyncResult BeginReadFromWeb( AsyncCallback a, string url )
  {
     var result = new AsyncResult<string>( a, null, this, "ReadFromFile" );

     ThreadPool.QueueUserWorkItem( ( b ) =>
     {
        WebClient wc = new WebClient();
        result.SetResult( wc.DownloadString( url ) );
        result.Complete( null );
     } );

     return result;
  }

  public string EndReadFromWeb( IAsyncResult result )
  {
     return AsyncResult<string>.End( result, this, "ReadFromFile" );
  }
4

3 回答 3

17

不,您的两个代码片段之间存在巨大差异。两者实际上都使用线程池,第一个当然是明确使用的。第二个以不太明显(和损坏)的方式进行,IAsyncResult 回调在线程池线程上执行。

线程池是一种共享资源,在大型程序中,TP 线程会有很多用途。不仅在您自己的代码中显式地使用它们,.NET Framework 也使用它们。对在线程池上运行的代码类型的指导是,它是快速执行的代码,并且不会进行任何使 TP 线程进入等待状态的阻塞调用。阻塞是以一种非常低效的方式使用非常昂贵的操作资源,并且会混淆其他可能正在使用 TP 线程的代码。线程池的一个重要部分是调度器,它试图将执行TP 线程的数量限制为机器可用的 CPU 内核数。

但是阻塞正是你在第一个片段中所做的。WebClient.DownloadString() 是一个非常慢的方法,它完成的速度不能超过您的 Internet 连接或线路另一端的服务器所允许的速度。实际上,您正在占用一个 TP 线程,可能有几分钟。根本没有做任何工作,它一直在等待 Socket.Read() 调用完成。有效的 CPU 核心利用率最多只有几个百分点。

当您使用 BeginXxxx() 或 XxxxAsync() 方法时,情况大不相同它在内部实现为一段代码,要求操作系统启动 I/O 操作。只需要几微秒。操作系统将请求传递给设备驱动程序,在 DownloadStringAsync() 的情况下是 TCP/IP 堆栈。它将作为数据项在 I/O 请求队列中的位置。您的电话很快就会返回。

最终,您的网卡从服务器获取数据,驱动程序完成 I/O 请求。通过几个层,让 CLR 获取另一个 TP 线程并运行您的回调。您可以快速地对数据执行任何操作,某种处理步骤通常也需要几微秒。

请注意区别,您的第一个代码占用 TP 线程为minutes,异步版本占用线程为microseconds。异步版本的扩展性更好,能够处理许多I/O 请求。

代码的异步版本的一个重要问题是正确编写要困难得多。同步版本中的局部变量需要成为异步版本中类的字段。调试起来也困难得多。这就是 .NET 获得 Task 类的原因,后来进一步扩展了对 C# 和 VB.NET 语言中的 async/await 关键字的支持。

于 2014-01-26T15:03:46.977 回答
6

让我们抛开自然异步的 IO 绑定操作,这些操作不需要专用线程来完成(参见Stephen Cleary 的There Is No Thread)。在池线程上执行自然异步API 的同步版本没有多大意义,因为您徒劳地阻塞了宝贵的资源:线程。DownloadString DownloadStringAsync

相反,让我们专注于 CPU 绑定的计算操作,这些操作确实需要专用线程。

首先, .NET Framework中没有标准AsyncResult<T>类。我相信,AsyncResult<string>您在代码中引用的实现取自 并发事务:实现Jeffrey Richter 的 CLR 异步编程模型文章。我也相信作者展示了如何实现AsyncResult<T> 教育目的,展示了 CLR 实现的样子。他在池线程上执行一项工作,ThreadPool.QueueUserWorkItem并实现IAsyncResult提供完成通知。可以在文章随附的LongTask.cs中找到更多详细信息。

所以,回答这个问题:

我真正想知道的是,当我们在那里有更好的替代 ThreadPool 时,为什么要使用 IAsyncResult ?

这不是一个“ IAsyncResultvs ThreadPool”案例。相反,在您的问题的上下文中,它IAsyncResult是对 的补充ThreadPool.QueueUserWorkItem,它提供了一种通知调用者工作项已完成执行的方法。ThreadPool.QueueUserWorkItemAPI 本身没有此功能,它只是返回指示bool工作项是否已成功排队等待池线程上的异步执行。

但是,对于这种情况,您根本不必实现AsyncResult<T>或使用ThreadPool.QueueUserWorkItem框架允许异步执行委托ThreadPool并跟踪完成状态,只需使用委托的BeginInvoke方法。这就是框架为委托实现异步编程模型 (APM) 模式的方式。例如,您可以使用以下方法执行一些 CPU 密集型工作BeginInvoke

static void Main(string[] args)
{
    Console.WriteLine("Enter Main, thread #" + Thread.CurrentThread.ManagedThreadId);

    // delegate to be executed on a pool thread
    Func<int> doWork = () =>
    {
        Console.WriteLine("Enter doWork, thread #" + Thread.CurrentThread.ManagedThreadId);
        // simulate CPU-bound work
        Thread.Sleep(2000);
        Console.WriteLine("Exit doWork");
        return 42;
    };

    // delegate to be called when doWork finished
    AsyncCallback onWorkDone = (ar) =>
    {
        Console.WriteLine("enter onWorkDone, thread #" + Thread.CurrentThread.ManagedThreadId);
    };

    // execute doWork asynchronously on a pool thread
    IAsyncResult asyncResult = doWork.BeginInvoke(onWorkDone, null); 

    // optional: blocking wait for asyncResult.AsyncWaitHandle
    Console.WriteLine("Before AsyncWaitHandle.WaitOne, thread #" + Thread.CurrentThread.ManagedThreadId);
    asyncResult.AsyncWaitHandle.WaitOne();

    // get the result of doWork
    var result = doWork.EndInvoke(asyncResult);
    Console.WriteLine("Result: " + result.ToString());

    // onWorkDone AsyncCallback will be called here on a pool thread, asynchronously 
    Console.WriteLine("Press Enter to exit");
    Console.ReadLine();
}

最后,值得一提的是,APM 模式正在被更方便且结构良好的基于​​任务的异步模式 (TAP)所取代。建议优先使用 TAP 模式,而不是其他更底层的 API 。

于 2014-01-29T00:24:59.647 回答
0

基本上这两种方式只是同一事物的不同行为。在 ThreadPool 上使用 IAsyncResult 的原因之一是返回值:Threading.WaitCallback 返回 void,因此您不能通过 ThreadPool.QueueUserWorkItem 调用直接返回任何值,但您可以使用 IAsyncResult 方法。

于 2014-01-26T14:26:20.317 回答