【问题标题】:IAsyncResult vs ThreadPoolIAsyncResult 与 ThreadPool
【发布时间】:2014-01-22 09:14: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" );
  }

【问题讨论】:

  • 哇哦...如果您对自己放在这里的内容如此兴奋...等到您到达Tasks 和async/await
  • 这里没有区别,因为您在第二个示例中仍然使用 ThreadPool 同步下载(在持续时间内阻塞线程)。当您在整个过程中使用异步调用,WebClient.DownloadStringAsync、HttpWebRequest.BeginGetResponse、Stream.BeginRead 等时,优势就会变得明显。
  • 与async的巨大区别在于,等待数据不会阻塞线程池线程,只有在接收或发送数据时,才会调用线程池上的endreceive。

标签: c# threadpool iasyncresult


【解决方案1】:

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

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

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

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

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

注意区别,您的第一个代码占用 TP 线程 分钟,异步版本占用线程 微秒。异步版本的可扩展性要好得多,能够处理许多 I/O请求。

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

【讨论】:

  • 在上面的示例中,BeginXXX 的内部实现也使用了 TP(而不是来自 BCL 的 DownloadXXXAsync),所以我认为无论执行网络请求需要多长时间,它仍会占用 TP。我正在自己编写 API,我无权将请求编组到硬件/操作系统级别并利用您上面提到的优势。在那种情况下,什么是更好的实现呢?因为我在显式使用 TP 或为我的 API 使用 BeginXXXX 之间陷入困境。如果我选择 BeginXXXX 然后使用普通线程执行请求会更好吗?
  • 我不太清楚你为什么不使用 WebClient.DownloadStringAsync()。
  • 对此表示歉意。也许我选择了一个错误的例子来说明我的问题。实现可以是数学计算或数据库调用(或在一段时间内阻止用户的任何东西)。
  • @user1928346 如果它是一个数学计算,它应该阻塞并同步,如果用户希望它不阻塞,让他们把长动作放在一个线程上。如果它是一个数据库调用,你应该同时公开同步和异步实现,如果你的数据库提供者同时支持,如果你的提供者只有同步调用,那么你应该只在你的 API 中公开同步调用。
  • 为什么 .Net 托管线程被认为是昂贵的?如果显式使用线程允许更清晰的代码,那么如果后续工作在队列中,则最好阻塞线程并依靠线程池使新线程可用。在我看来,线程池的目的,就像 db 连接池一样,是在方便的时候产生线程成本,以便它可以随时使用。
【解决方案2】:

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

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

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

所以,回答这个问题:

我真正想知道的是,当我们有办法时,为什么要使用 IAsyncResult 那里有更好的替代线程池?

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

但是,对于这种情况,您根本不必实现 AsyncResult&lt;T&gt; 或使用 ThreadPool.QueueUserWorkItem框架允许在 ThreadPool 上异步执行委托并跟踪完成状态,只需使用委托的 BeginInvoke 方法即可。这就是框架为代表实现Asynchronous Programming Model (APM) pattern 的方式。例如,以下是使用 BeginInvoke 执行一些 CPU 密集型工作的方法:

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 模式正在被更方便且结构良好的Task-based Asynchronous Pattern (TAP) 所取代。 recommended TAP 模式应该比其他更底层的 API 更受青睐。

【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多