揭秘 Task.Wait
简介
Task.Wait 是 Task 的一个实例方法,用于等待 Task 完成,如果 Task 未完成,会阻塞当前线程。
非必要情况下,不建议使用 Task.Wait,而应该使用 await。
本文将基于 .NET 6 的源码来分析 Task.Wait 的实现,其他版本的实现也是类似的。
var task = Task.Run(() =>
{
Thread.Sleep(1000);
return "Hello World";
});
var sw = Stopwatch.StartNew();
Console.WriteLine("Before Wait");
task.Wait();
Console.WriteLine("After Wait: {0}ms", sw.ElapsedMilliseconds);
Console.WriteLine("Result: {0}, Elapsed={1}ms", task.Result, sw.ElapsedMilliseconds);
输出:
Before Wait
After Wait: 1002ms
Result: Hello World, Elapsed=1002ms
可以看到,task.Wait 阻塞了当前线程,直到 task 完成。
其效果等效于:
task.Result (仅限于 Task<TResult>)
task.GetAwaiter().GetResult()
task.Wait 共有 5 个重载
public class Task<TResult> : Task
{
}
public class Task
{
// 1. 无参数,无返回值,阻塞当前线程至 task 完成
public void Wait()
{
Wait(Timeout.Infinite, default);
}
// 2. 无参数,有返回值,阻塞当前线程至 task 完成或 超时
// 如果超时后 task 仍未完成,返回 False,否则返回 True
public bool Wait(TimeSpan timeout)
{
return Wait((int)timeout.TotalMilliseconds, default);
}
// 3. 和 2 一样,只是参数类型不同
public bool Wait(int millisecondsTimeout)
{
return Wait(millisecondsTimeout, default);
}
// 4. 无参数,无返回值,阻塞当前线程至 task 完成或 cancellationToken 被取消
// cancellationToken 被取消时抛出 OperationCanceledException
public void Wait(CancellationToken cancellationToken)
{
Wait(Timeout.Infinite, cancellationToken);
}
// 5. 无参数,有返回值,阻塞当前线程至 task 完成或 超时 或 cancellationToken 被取消
// 如果超时后 task 仍未完成,返回 False,否则返回 True
// cancellationToken 被取消时抛出 OperationCanceledException
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
{
ThrowIfContinuationIsNotNull();
return InternalWaitCore(millisecondsTimeout, cancellationToken);
}
}
下面是一个使用 bool Wait(int millisecondsTimeout) 的例子:
var task = Task.Run(() =>
{
Thread.Sleep(1000);
return "Hello World";
});
var sw = Stopwatch.StartNew();
Console.WriteLine("Before Wait");
bool completed = task.Wait(millisecondsTimeout: 200);
Console.WriteLine("After Wait: completed={0}, Elapsed={1}", completed, sw.ElapsedMilliseconds);
Console.WriteLine("Result: {0}, Elapsed={1}", task.Result, sw.ElapsedMilliseconds);
输出:
Before Wait
After Wait: completed=False, Elapsed=230
Result: Hello World, Elapsed=1001
因为指定的 millisecondsTimeout 不足以等待 task 完成,所以 task.Wait 返回 False,继续执行后续代码。
但是,task.Result 仍然会阻塞当前线程,直到 task 完成。
关联的方法还有 Task.WaitAll 和 Task.WaitAny。同样也是非必要情况下,不建议使用。
背后的实现
task.Wait、task.Result、task.GetAwaiter().GetResult() 这三者背后的实现其实是一样的,都是调用了 Task.InternalWaitCore 这个实例方法。
借助 Rider 的类库 debug 功能,来给大家展示一下这三种方法的调用栈。
Task<string> RunTask()
{
return Task.Run(() =>
{
Thread.Sleep(1000);
return "Hello World!";
});
}
var task1 = RunTask();
task1.Wait();
var task2 = RunTask();
task2.GetAwaiter().GetResult();
var task3 = RunTask();
_ = task3.Result;



Task.InternalWaitCore 是 Task 的一个私有实例方法。
public class Task
{
internal bool InternalWait(int millisecondsTimeout, CancellationToken cancellationToken) =>
InternalWaitCore(millisecondsTimeout, cancellationToken);
private bool InternalWaitCore(int millisecondsTimeout, CancellationToken cancellationToken)
{
// 如果 Task 已经完成,直接返回 true
bool returnValue = IsCompleted;
if (returnValue)
{
return true;
}
// 如果调用的是 Task.Wait 的无参重载方法,且Task 已经完成或者在内联执行后完成,直接返回 true,不会阻塞 Task.Wait 的调用线程。
// WrappedTryRunInline 的意思是尝试在捕获的 TaskScheduler 中以内联的方式执行 Task,此处不展开
if (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled &&
WrappedTryRunInline() && IsCompleted)
{
returnValue = true;
}
else
{
// Task 未完成,调用 SpinThenBlockingWait 方法,阻塞当前线程,直到 Task 完成或超时或 cancellationToken 被取消
returnValue = SpinThenBlockingWait(millisecondsTimeout, cancellationToken);
}
return returnValue;
}
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
bool infiniteWait = millisecondsTimeout == Timeout.Infinite;
uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount;
bool returnValue = SpinWait(millisecondsTimeout);
if (!returnValue)
{
var mres = new SetOnInvokeMres();
try
{
// 将 mres 作为 Task 的 Continuation,当 Task 完成时,会调用 mres.Set() 方法
AddCompletionAction(mres, addBeforeOthers: true);
if (infiniteWait)
{
bool notifyWhenUnblocked = ThreadPool.NotifyThreadBlocked();
try
{
// 没有指定超时时间,阻塞当前线程,直到 Task 完成或 cancellationToken 被取消
returnValue = mres.Wait(Timeout.Infinite, cancellationToken);
}
finally
{
if (notifyWhenUnblocked)
{
ThreadPool.NotifyThreadUnblocked();
}
}
}
else
{
uint elapsedTimeTicks = ((uint)Environment.TickCount) - startTimeTicks;
if (elapsedTimeTicks < millisecondsTimeout)
{
bool notifyWhenUnblocked = ThreadPool.NotifyThreadBlocked();
try
{
// 指定了超时时间,阻塞当前线程,直到 Task 完成或 超时 或 cancellationToken 被取消
returnValue = mres.Wait((int)(millisecondsTimeout - elapsedTimeTicks), cancellationToken);
}
finally
{
if (notifyWhenUnblocked)
{
ThreadPool.NotifyThreadUnblocked();
}
}
}
}
}
finally
{
// 如果因为超时或 cancellationToken 被取消,而导致 Task 未完成,需要将 mres 从 Task 的 Continuation 中移除
if (!IsCompleted) RemoveContinuation(mres);
}
}
return returnValue;
}
private bool SpinWait(int millisecondsTimeout)
{
if (IsCompleted) return true;
if (millisecondsTimeout == 0)
{
// 如果指定了超时时间为 0,直接返回 false
return false;
}
// 自旋至少一次,总次数由 Threading.SpinWait.SpinCountforSpinBeforeWait 决定
// 如果 Task 在自旋期间完成,返回 true
int spinCount = Threading.SpinWait.SpinCountforSpinBeforeWait;
SpinWait spinner = default;
while (spinner.Count < spinCount)
{
// -1 表示自旋期间不休眠,不会让出 CPU 时间片
spinner.SpinOnce(sleep1Threshold: -1);
if (IsCompleted)
{
return true;
}
}
// 自旋结束后,如果 Task 仍然未完成,返回 false
return false;
}
private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
{
// 往父类 ManualResetEventSlim 中传入 false,表示 ManualResetEventSlim 的初始状态为 nonsignaled
// 也就是说,在调用 ManualResetEventSlim.Set() 方法之前,ManualResetEventSlim.Wait() 方法会阻塞当前线程
internal SetOnInvokeMres() : base(false, 0) { }
public void Invoke(Task completingTask) { Set(); }
public bool InvokeMayRunArbitraryCode => false;
}
}
Task.Wait 的两个阶段
SpinWait 阶段
用户态锁,不能维持很长时间的等待。线程在等待锁的释放时忙等待,不会进入休眠状态,从而避免了线程切换的开销。它在自旋等待期间会持续占用CPU时间片,如果自旋等待时间过长,会浪费CPU资源。
BlockingWait 阶段
内核态锁,在内核态实现的锁机制。当线程无法获得锁时,会进入内核态并进入休眠状态,将CPU资源让给其他线程。线程在内核态休眠期间不会占用CPU时间片,从而避免了持续的忙等待。当锁可用时,内核会唤醒休眠的线程并将其调度到CPU上执行。
BlockingWait 阶段 主要借助 SetOnInvokeMres 实现, SetOnInvokeMres 继承自 ManualResetEventSlim。
它会阻塞调用线程直到 Task 完成 或 超时 或 cancellationToken 被取消。
当前线程,Task 完成时,SetOnInvokeMres.Set() 方法会被当做 Task 的回调被调用从而解除阻塞。
Task.Wait 可能会导致的问题
到目前为止,我们已经了解到 Task.Wait 阻塞当前线程等待 Task 完成的原理,但是我们还是没有回答最开始的问题:为什么不建议使用 Task.Wait。
可能会导致线程池饥饿
线程池饥饿是指线程池中的可用线程数量不足,无法执行任务的现象。
在 ThreadPool 的设计中,如果已经创建的线程达到了一定数量,就算有新的任务需要执行,也不会立即创建新的线程(每 500ms 才会检查一次是否需要创建新的线程)。
更详细的介绍可以参考我的另一篇文章:https://www.cnblogs.com/eventhorizon/p/15316955.html#3-避免饥饿机制starvation-avoidance
如果我们在一个 ThreadPool 线程中调用 Task.Wait,而 Task.Wait 又阻塞了这个线程,无法执行其他任务,这样就会导致线程池中的可用线程数量不足,从而阻塞了任务的执行。
可能会导致死锁
除此之外 Task.Wait 也可能会导致死锁,这里就不展开了。具体可以参考:https://www.cnblogs.com/eventhorizon/p/15912383.html#同步上下文synchronizationcontext导致的死锁问题与-taskconfigureawaitcontinueoncapturedcontextfalse
.NET 6 对 Task.Wait 的优化
细心的同学会注意到 SpinThenBlockingWait 的 BlockingWait 阶段,会调用 ThreadPool.NotifyThreadBlocked() 方法,这个方法会通知线程池当前线程被阻塞了,新的线程会被立即创建出来。
但这也不代表 Task.Wait 就可以放心使用了,ThreadPool 中的线程被大量阻塞,就算借助 ThreadPool.NotifyThreadBlocked() 能让新任务继续执行,但这会导致线程频繁的创建和销毁,导致性能下降。
总结
Task.Wait 对调用线程的阻塞分为两个阶段:SpinWait 阶段 和 BlockingWait 阶段。如果 Task 完成较快,就可以在性能较好的 SpinWait 阶段完成等待。
滥用 Task.Wait 会导致线程池饥饿或死锁。
.NET 6 对 Task.Wait 进行了优化,如果 Task.Wait 阻塞了 ThreadPool 中的线程,会立即创建新的线程,避免了线程池中的可用线程数量不足的问题。但是这也会导致线程频繁的创建和销毁,导致性能下降。
揭秘 Task.Wait的更多相关文章
- .NET Task揭秘(一)
Task为.NET提供了基于任务的异步模式,它不是线程,它运行在线程池的线程上.本着开源的精神, 本文以解读基于.NET4.5 Task源码的方式来揭秘Task的实现原理. Task的创建 Tas ...
- .NET Task 揭秘(3)async 与 AsyncMethodBuilder
目录 前言 AsyncMethodBuilder 介绍 AsyncMethodBuilder 是状态机的重要组成部分 AsyncMethodBuilder 的结构 AsyncMethodBuilder ...
- 学习ASP.NET Web API框架揭秘之“HTTP方法重写”
最近在看老A的<ASP.NET Web API 框架揭秘>,这本书对于本人现阶段来说还是比较合适的(对于调用已经较为熟悉,用其开发过项目,但未深入理解过很多内容为何可以这样“调用”).看到 ...
- Spark Tungsten揭秘 Day3 内存分配和管理内幕
Spark Tungsten揭秘 Day3 内存分配和管理内幕 恭喜Spark2.0发布,今天会看一下2.0的源码. 今天会讲下Tungsten内存分配和管理的内幕.Tungsten想要工作,要有数据 ...
- Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析
Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析 今天通过集群运行模式观察.研究和透彻的刨析SparkStreaming的日志和web监控台. Day28 ...
- Spark Streaming揭秘 Day17 资源动态分配
Spark Streaming揭秘 Day17 资源动态分配 今天,让我们研究一下一个在Spark中非常重要的特性:资源动态分配. 为什么要动态分配?于Spark不断运行,对资源也有不小的消耗,在默认 ...
- Spark Streaming揭秘 Day4-事务一致性(Exactly one)
Spark Streaming揭秘 Day4 事务一致性Exactly one 引子 对于业务处理系统,事务的一致性非常的关键,事务一致性(Exactly one),简单来说,就是输入数据一定会被处理 ...
- ASP.NET Web API框架揭秘:路由系统的几个核心类型
ASP.NET Web API框架揭秘:路由系统的几个核心类型 虽然ASP.NET Web API框架采用与ASP.NET MVC框架类似的管道式设计,但是ASP.NET Web API管道的核心部分 ...
- 第四节:Task的启动的四种方式以及Task、TaskFactory的线程等待和线程延续的解决方案
一. 背景 揭秘: 在前面的章节介绍过,Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面 ...
- 第五节:Task构造函数之TaskCreationOptions枚举处理父子线程之间的关系。
一. 整体说明 揭秘: 通过F12查看Task类的源码(详见下面的截图),发现Task类的构造函数有有一个参数为:TaskCreationOptions类型,本章节可以算作是一个扩展章节,主要就来研究 ...
随机推荐
- selenium验证码处理之机器学习(光学识别ocr技术获取验证码的数据)
ocr识别库地址: https://github.com/UB-Mannheim/tesseract/wiki 遇到的问题:百度的解释------------------- 遇到的问题2:
- 开源项目audioFlux: 针对音频领域的深度学习工具库
目录 时频变换 频谱重排 倒谱系数 解卷积 谱特征 音乐信息检索 audioFlux是一个Python和C实现的库,提供音频领域系统.全面.多维度的特征提取与组合,结合各种深度学习网络模型,进行音频领 ...
- Express实现定时发送邮件
在开发中我们有时候需要每隔 一段时间发送一次电子邮件,或者在某个特定的时间进行发送邮件, 无需手动去操作,基于这样的情况下我们需要用到了定时任务,一般可以写个定时器,来完成相应的需求,在 node.j ...
- 这个小项目,上周被国外 AI 新闻网站报道,前些天又上了 github 热榜
疫情期间在校花了几个月时间,写了这个小项目,是关于音频特征提取和分析的,自己是 AI 专业研究音频的,但受限于对音频特征的理解,做研究时总感觉缺乏"底料",所以当做是学习练手做了这 ...
- JMeter-BeanShell预处理程序和BeanShell后置处理程序的应用
一.什么是BeanShell? BeanShell是用Java写成的,一个小型的.免费的.可以下载的.嵌入式的Java源代码解释器,JMeter性能测试工具也充分接纳了BeanShell解释器,封装成 ...
- go-easy-utils 2.0 正式发布,全面支持泛型和any
介绍 这是一个基于 Go 语言开发的通用数据类型处理工具类,帮助开发者在业务代码实现中处理常见的数据类型和数据操作.可以让您专注于您的业务代码的实现,而免去处理基本数据类型转换和验证的功能.该工具库无 ...
- ASP.NET Core - 缓存之分布式缓存
分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护. 分布式缓存可以提高 ASP.NET Core 应用的性能和可伸缩性,尤其是当应用由云服务或服务器场托管时. 与其 ...
- docker上面部署nginx-waf 防火墙“modsecurity”,使用CRS规则,搭建WEB应用防火墙
web防火墙(waf)免费开源的比较少,并且真正可以商用的WAF少之又少,modsecurity 是开源防火墙鼻祖并且有正规公司在维护着,目前是https://www.trustwave.com在维护 ...
- API 接口主流协议有哪些?如何创建 HTTP/HTTP、WebSocket/WebSockets、TCP/UDP、gRPC、SOAP、Dubbo/HSF 等不同协议?
API 接口协议繁多,不同的协议有着不同的使用场景.70% 互联网应用开发者日常仅会接触到最通用的 HTTP 协议,相信大家希望了解更多其他协议的信息.我们今天会给大家介绍各种 API 接口主流协议和 ...
- vue—一个组件调用另一个组件的methods
这种方法不常用,项目中有个地方共享数据了,起初没用vuex做,后来有个地方不好解决,这两个组件没有什么关系 1.首先同一个vue实例来调用两个方法.所以可以建立一个中转站. 建立 util.js 中转 ...