如果.NET任务线程正在等待异步操作完成,那么它的资源是否会暂时返回到池中?

12
我有一个TPL任务,它有两个功能。首先,它调用一个Web服务。其次,它将一些数据插入到数据库中。我最多同时启动20个任务,一遍又一遍地执行同样的操作。它们整天都在调用Web服务并将数据插入到数据库中。
我在.NET中相当新于TPL。我已经使用过后台工作进程和异步Web服务。
Web服务调用和数据库插入都是阻塞调用,在任务正在运行的线程内部进行。
我理解,在使用任务时,.NET为您管理一个线程池。是这样吗?
如果我使用async和await()进行异步调用而不是进行阻塞调用,线程池是否会拥有更多可用的线程?
我的理论是(我不确定为什么这么想),当线程在等待阻塞的Web服务时,它忙于无事可做,并且不能暂时将其资源返回到池中。但是,我想知道,如果任务正在等待异步调用完成,主要任务线程是否能够切换以便让其他内容进行处理。
我的理论正确吗?还是我在凭空想象?
我使用C#和.NET 4.0,但如果需要,我可以使用4.5。

你是正确的,你当前的实现并不是最优的(如果它足够好,你只有在遇到问题时才会进行更改)。但是为了回答你的第二个问题,你需要提供一个例子,说明如何在任务中使用异步。理论上,线程将被返回到线程池,但是有很多方法会出错。 - Stilgar
谢谢Stilgar。我现在要去睡觉了,西雅图时间是凌晨1点。但是明天我会发布更多的代码,然后删除这个评论。 - Trevor
@Trevor:我认为需要牢记的关键一点是,有两种类型的任务。我称它们为Promise任务和Delegate任务。只有Delegate任务才会在线程上执行;Promise任务只是某个未来事件的抽象表示。TPL几乎完全使用Delegate任务;async/await 几乎完全使用Promise任务。请查看我的博客文章获取更多信息。 - Stephen Cleary
3个回答

7
如果我使用async和await()调用服务和数据库,而不是阻塞调用,线程池是否会有更多的线程可用?
这取决于您所说的“利用async-await”的含义。
当您使用Task.Run时,幕后,Task类使用ThreadPool来使用ThreadPool线程卸载工作。
如果您的服务没有公开真正的异步API,并且您使用Task.Run排队工作,则无论是否使用async-await,您仍将阻止ThreadPool线程执行IO绑定的工作。在您的问题中,您声明两个调用都是阻塞调用,在这种情况下,答案是否定的,用于进行这些阻塞调用的ThreadPool线程仍将被阻塞。
如果您的服务和数据库调用是真正的异步API(不需要消耗任何额外的线程来完成其工作),则可以利用async-await,因为当您等待其中一个调用(您根本不需要使用Task.Run)时,当前线程将控制权返回给调用者,并且可以同时用于做更多的工作。如果是这种情况,则是的。
您的理论是正确的。如果排队的ThreadPool工作的主要工作是进行IO绑定请求,则它花费大部分时间仅阻塞直到请求完成。
当您等待Task时,控件会返回给调用者。假设您的服务调用是REST调用,则可以使用HttpClient,它公开真正的非线程消耗异步方法,例如GetAsync,PostAsync,当您等待这些调用时,您的调用线程将被释放以在此期间执行更多工作。

当你说IO绑定的工作时,这是否包括数据库插入操作? - Trevor
1
是的,数据库调用是IO绑定的。现在大多数ORM都公开了异步端点(EF6,ADO.NET)。 - Yuval Itzchakov

2
如果一个应用程序的所有任务都被阻塞,每个任务将使用线程池中的一个线程。如果所有任务定期等待,则线程池不需要为每个任务使用一个线程。当您的代码等待尚未完成的操作时,该方法的状态将被保存,以便可以在任何其他线程上恢复它。空闲的线程池线程会在一段时间后被释放,因此调用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%。


1
答案是肯定的。虽然严格来说它并不是在“等待”异步操作完成(否则异步就没有好处了)。在幕后,有一个回调委托会在异步操作完成时运行,这就允许你的调用线程在不阻塞的情况下继续进行。正是async/await的魔法将这些“延续”转化为一个线性的代码块。
由于你使用的是线程池线程,在遇到await时,线程会返回到线程池中。需要注意的是,通常情况下,当等待的操作完成时,它会尝试回到它启动的线程上(现在可能正在被另一个任务使用),所以你可能会观察到获取结果方面的延迟问题,因为线程池线程现在已经被占用来启动其他任务。随着时间的推移,线程池会尝试调整可用线程的数量以满足需求,但如果你的工作突然增加,你可能会发现这不会很快发生。结果将是表现看起来很差,因为你可能只有少量的可用线程。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接