- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
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 的一个私有实例方法.
https://github.com/dotnet/runtime/blob/c76ac565499f3e7c657126d46c00b67a0d74832c/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L2883 。
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;
}
}
用户态锁,不能维持很长时间的等待。线程在等待锁的释放时忙等待,不会进入休眠状态,从而避免了线程切换的开销。它在自旋等待期间会持续占用CPU时间片,如果自旋等待时间过长,会浪费CPU资源.
内核态锁,在内核态实现的锁机制。当线程无法获得锁时,会进入内核态并进入休眠状态,将CPU资源让给其他线程。线程在内核态休眠期间不会占用CPU时间片,从而避免了持续的忙等待。当锁可用时,内核会唤醒休眠的线程并将其调度到CPU上执行.
BlockingWait 阶段 主要借助 SetOnInvokeMres 实现, SetOnInvokeMres 继承自 ManualResetEventSlim。 它会阻塞调用线程直到 Task 完成 或 超时 或 cancellationToken 被取消.
当前线程,Task 完成时,SetOnInvokeMres.Set() 方法会被当做 Task 的回调被调用从而解除阻塞.
到目前为止,我们已经了解到 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 。
细心的同学会注意到 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的文章就讲到这里了,如果你想了解更多关于揭秘Task.Wait的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
前言 Vue和Vite之父尤雨溪宣布成立公司 VoidZero,目前已经融资3200万。这篇文章欧阳将带你了解VoidZero是如何改变javascript的世界! 关注公众号:【前端欧阳】,给自
前言 TimerQueue 是.NET中实现定时任务的核心组件,它是一个定时任务的管理器,负责存储和调度定时任务。它被用于实现很多 .NET 中的定时任务,比如 System.Threadin
网站服务器这类问题我相信很多朋友讲过,但为什么他和网站优化还有关系呢?那么请您读读我写的没有文笔的文章,看看是否有所认同。 是的,服务器其实也是一个优化网站的最大杀手也是最大帮手,现今万网与阿里云
这个问题在这里已经有了答案: How does XPath deal with XML namespaces? (2 个回答) 5年前关闭。 我有这个 XML
在我们工作中无数次点击鼠标的时候,你有思考过鼠标是怎么工作的吗?在购买鼠标的时候你是怎么挑选的呢?看着那些标称的数据,你是否茫然过?那么如果小编现在说,其实每个鼠标都是一台优秀的“照相机”,你会相信
在浙江绍兴的山姆超市外,“黄牛”现象引发了广泛关注。这些“黄牛”通过提供带入和结账服务,让未办理会员卡的消费者也能进入超市购物。这一行为不仅扰乱了市场秩序,也对山姆会员商店的会员管理系统提出了挑战。今
在 this question海报询问如何在一行中执行以下操作: sub my_sub { my $ref_array = shift; my @array = @$ref_array
我正在尝试了解 Javascript Lambda 方法,但我仍然对非常灵活的定义方式和变量范围感到非常困惑。 例如我正在研究以下插件的代码,除了代码中还有其他部分我不太清楚,我感兴趣的是代码是如何组
谁能帮我揭开以下表达式的神秘面纱: ++[[]][+[]]+[+[]] 我的理解从左到右: ++[[]]:不确定这将评估什么以及如何评估。 [+[]]:+[] 将首先执行,一元运算符将尝试将 [] 转
在 python 中,可以在多个进程之间共享 ctypes 对象。但是我注意到分配这些对象似乎非常昂贵。 考虑以下代码: from multiprocessing import sharedctype
我目前对 Glassfish 3.1.2.2 处理 EJB 的方式感到困惑。 我有一个 OSGi 项目,它由许多 OSGi 包(jar)组成。此外,还有一些 WAR,包括 Tapestry Web 应
这些天我在玩线程库并尝试实现一些功能。其中一个教程说要运行程序使用: gcc -lpthread -lrt -lc -lm project1.c scheduler.c -o out 首先我需要深入了
如 app.secret_key未设置,Flask 将不允许您设置或访问 session 字典。 这就是flask user guide has to say在这个问题上。 我对 Web 开发很陌生,
Ruby on Rails 新手问题... 考虑以下代码(在 View 中): | | 在 ERB 标签中调用的一些方法对我来说就像魔法一样,我正试图揭开它们的神秘面纱。如果我不了解代码
HTTPS 是建立在 SSL/TLS 传输层安全协议之上的一种 HTTP 协议,相当于 HTTPS = HTTP + SSL/TLS。第一篇文章 “HTTPS - 通俗易懂的阐述 HTTPS 协
本周新 Xcode 3.2.1 中包含的自述文件内容如下: 静态代码分析通过“构建”菜单下的“构建和分析”选项或通过自定义build设置完全集成在 Xcode IDE 中 GCC 4.2 是 10.6
是的,我知道。关于 NSOperation 世界有很多问题和答案,但我仍然有一些疑问。我会尝试用两部分的问题来解释我的疑虑。它们相互关联。 在 SO 帖子中 nsoperationqueue-and-
运行后,我看到我的应用程序在 TaskMgr 中占用了 3.5Gb 我在 Windbg 中看到的内容有点令人困惑: 0:022> !address -summary ProcessParametrs
我是一名优秀的程序员,十分优秀!