【问题标题】:Are a .NET Task thread's resources returned back to the pool temporarily if the thread is waiting on an async operation to finish?如果线程正在等待异步操作完成,.NET 任务线程的资源是否会暂时返回到池中?
【发布时间】:2014-08-15 07:43:34
【问题描述】:

我有一个 TPL 任务,它做两件事。首先,它调用 Web 服务。其次,它将一些数据插入数据库。我有多达 20 个任务一次开始,一遍又一遍地做同样的事情。他们整天所做的就是调用 Web 服务并将数据插入数据库。

我对 .NET 中的 TPL 还很陌生。我用后台工作进程和异步 Web 服务做了一些事情。

Web 服务调用和数据库插入都是在任务运行的线程中的阻塞调用。

我知道,当您使用 Tasks 时,.NET 会在幕后为您管理一个线程池。是吗?

如果我使用 async 和 await() 进行服务调用和数据库调用而不是让它们阻塞调用,线程池是否会有更多线程可供使用?

我的理论(我不确定我为什么这么认为)是线程在等待阻塞的 Web 服务时忙于无所事事,并且无法暂时将其资源返回到池中。但我想知道任务是否正在等待异步调用完成主任务线程是否能够切换以让其他东西在等待时处理。

我的理论对吗?还是我在编造?

我正在使用 c# 和 .NET 4.0,但如果需要,我可以转到 4.5。

【问题讨论】:

  • 你是对的,你当前的实现不是最优的(它可能已经足够好了,所以你只有在发现问题时才改变它)。但是,要回答您的第二个问题,您必须提供一个示例,您将如何在任务中使用异步。理论上线程会被返回到池中,但是有很多方法可以让它出错。
  • 谢谢斯蒂尔加。我现在要去睡觉了。西雅图时间凌晨 1:00。但是我明天会发布更多代码,然后删除这条评论。
  • @Trevor:我认为要记住的关键是有两种任务。我称它们为 Promise Tasks 和 Delegate Tasks。只有委托任务会在线程上执行; Promise 任务只是一些未来事件的抽象表示。 TPL 几乎完全使用委托任务; async/await 几乎完全使用 Promise 任务。查看my blog post 了解更多信息。

标签: c# .net asynchronous task-parallel-library async-await


【解决方案1】:

如果我创建线程池是否有更多线程可供使用? 使用 async 和 await() 代替的服务调用和数据库调用 让他们阻止通话?

这取决于您所说的“使用异步等待”

当您使用Task.Run 时,在幕后,Task 类使用ThreadPool 使用ThreadPool 线程卸载工作。

如果您的服务没有公开真正的异步 api,并且您使用 Task.Run 来排队您的工作,那么无论使用 async-await,您仍然会阻塞线程池线程来执行 IO 绑定工作。在您的问题中,您声明两个调用都是阻塞调用,在这种情况下答案是否定的,用于进行这些阻塞调用的线程池线程仍将被阻塞。

如果您的服务和数据库调用是真正的异步 API(不消耗任何额外线程来完成其工作的 API),您可以利用 async-await,就像您在其中一个调用上 await 时一样(以及您根本不需要对它们使用 Task.Run),当前线程将把控制权交还给调用者,同时可以用来做更多的工作。如果是这种情况,那么是的。

我的理论(我不确定我为什么这么认为)是线程在等待阻塞的 Web 服务时忙于无所事事,并且无法暂时将其资源返回到池中。但我想知道任务是否正在等待异步调用完成主任务线程是否能够切换以让其他东西在等待时处理。

你的理论是正确的。如果排队线程池的主要工作是发出一个 IO 绑定请求,那么它花费大部分时间只是阻塞,直到请求完成。

当您 awaitTask 时,控制权返回给调用者。假设您的服务调用是 REST 调用,您可以使用 HttpClient,它公开真正的非线程消耗异步方法,例如 GetAsyncPostAsync,当您 await 这些调用时,您的调用线程被释放到同时做更多的工作。

【讨论】:

  • 当你说 IO 绑定的工作是否包括数据库插入?
  • 是的,数据库调用是 IO 绑定的。当今大多数 ORM 都公开了异步端点(EF6、ADO.NET)
【解决方案2】:

如果应用程序的所有任务都阻塞,则每个任务将使用线程池中的一个线程。

如果所有任务都定期await,线程池不需要为每个任务使用一个线程。

当您的代码awaits 的操作尚未完成时,该方法的状态将被保存,以便它可以在任何其他线程上恢复。

空闲线程池线程在一段时间后被释放,因此在调用await 的方法仍在运行时,可以从线程池中释放命中await 的实际线程。

将所有这些放在一起,例程的异步版本可以用更少的线程完成相同的工作(假设工作负载在等待与旋转 CPU 的时间之间有足够的平衡)。

此代码运行 100 个同步等待的任务:

var numTasks = 100;
for (int i = 0; i < numTasks; i++)
{
    Thread.Sleep(5);
    Task.Run(() =>
    {
        Thread.Sleep(5000);
        Interlocked.Decrement(ref numTasks);
    });
}
while (numTasks > 0) Thread.Sleep(100);

对于异步等待,将其更改为:

    Task.Run(async () =>
    {
        await Task.Delay(5000);
        Interlocked.Decrement(ref numTasks);
    });

在我的系统上,异步版本将峰值线程数增加了一半,并且需要 20% 的时间来完成相同的“工作”。

【讨论】:

    【解决方案3】:

    答案是肯定的。尽管从技术上讲,它不是“等待”异步操作完成(否则异步将没有任何好处)。在后台,有一个回调委托在异步操作完成时运行,这允许您的调用线程在不阻塞的情况下继续进行。正是 async/await 魔法将这些“延续”变成了一段线性的代码。

    因为您使用的是线程池线程,所以当它遇到等待时,线程将返回到线程池。这里要小心的是,正常行为是当等待的操作完成时,它会尝试回到启动它的线程(现在可能被另一个任务使用),因此您可能会在获取结果返回,因为线程池线程现在被捆绑启动其他任务。随着时间的推移,线程池将尝试调整可用线程的数量以满足需求,但如果您的工作是突发的,您可能会发现这不会很快发生。结果显然性能很差,因为您可能只有少量可用线程。

    【讨论】:

      猜你喜欢
      • 2015-07-13
      • 2011-06-02
      • 1970-01-01
      • 2018-04-16
      • 1970-01-01
      • 2019-10-24
      • 1970-01-01
      • 2015-08-29
      • 2010-10-16
      相关资源
      最近更新 更多