简介

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 完成。

其效果等效于:

  1. task.Result (仅限于 Task<TResult>)

  2. 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;
}
}

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() 能让新任务继续执行,但这会导致线程频繁的创建和销毁,导致性能下降。

总结

  1. Task.Wait 对调用线程的阻塞分为两个阶段:SpinWait 阶段 和 BlockingWait 阶段。如果 Task 完成较快,就可以在性能较好的 SpinWait 阶段完成等待。

  2. 滥用 Task.Wait 会导致线程池饥饿或死锁。

  3. .NET 6 对 Task.Wait 进行了优化,如果 Task.Wait 阻塞了 ThreadPool 中的线程,会立即创建新的线程,避免了线程池中的可用线程数量不足的问题。但是这也会导致线程频繁的创建和销毁,导致性能下降。

揭秘 Task.Wait的更多相关文章

  1. .NET Task揭秘(一)

    Task为.NET提供了基于任务的异步模式,它不是线程,它运行在线程池的线程上.本着开源的精神, 本文以解读基于.NET4.5 Task源码的方式来揭秘Task的实现原理.   Task的创建 Tas ...

  2. .NET Task 揭秘(3)async 与 AsyncMethodBuilder

    目录 前言 AsyncMethodBuilder 介绍 AsyncMethodBuilder 是状态机的重要组成部分 AsyncMethodBuilder 的结构 AsyncMethodBuilder ...

  3. 学习ASP.NET Web API框架揭秘之“HTTP方法重写”

    最近在看老A的<ASP.NET Web API 框架揭秘>,这本书对于本人现阶段来说还是比较合适的(对于调用已经较为熟悉,用其开发过项目,但未深入理解过很多内容为何可以这样“调用”).看到 ...

  4. Spark Tungsten揭秘 Day3 内存分配和管理内幕

    Spark Tungsten揭秘 Day3 内存分配和管理内幕 恭喜Spark2.0发布,今天会看一下2.0的源码. 今天会讲下Tungsten内存分配和管理的内幕.Tungsten想要工作,要有数据 ...

  5. Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析

    Spark Streaming揭秘 Day30 集群模式下SparkStreaming日志分析 今天通过集群运行模式观察.研究和透彻的刨析SparkStreaming的日志和web监控台. Day28 ...

  6. Spark Streaming揭秘 Day17 资源动态分配

    Spark Streaming揭秘 Day17 资源动态分配 今天,让我们研究一下一个在Spark中非常重要的特性:资源动态分配. 为什么要动态分配?于Spark不断运行,对资源也有不小的消耗,在默认 ...

  7. Spark Streaming揭秘 Day4-事务一致性(Exactly one)

    Spark Streaming揭秘 Day4 事务一致性Exactly one 引子 对于业务处理系统,事务的一致性非常的关键,事务一致性(Exactly one),简单来说,就是输入数据一定会被处理 ...

  8. ASP.NET Web API框架揭秘:路由系统的几个核心类型

    ASP.NET Web API框架揭秘:路由系统的几个核心类型 虽然ASP.NET Web API框架采用与ASP.NET MVC框架类似的管道式设计,但是ASP.NET Web API管道的核心部分 ...

  9. 第四节:Task的启动的四种方式以及Task、TaskFactory的线程等待和线程延续的解决方案

    一. 背景 揭秘: 在前面的章节介绍过,Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面 ...

  10. 第五节:Task构造函数之TaskCreationOptions枚举处理父子线程之间的关系。

    一. 整体说明 揭秘: 通过F12查看Task类的源码(详见下面的截图),发现Task类的构造函数有有一个参数为:TaskCreationOptions类型,本章节可以算作是一个扩展章节,主要就来研究 ...

随机推荐

  1. ACM-NEFUOJ-P239回文数

    #include<bits/stdc++.h> using namespace std; int n,p[1000],len,p1[1000]; int f() { int i; for( ...

  2. [ACM]TL-Kruskal

    #include<iostream> #include<cstdio> using namespace std; struct edge { int u; int v; int ...

  3. Wikijs简介-强大&可扩展的开源维基软件

    Wikijs - 最强大 最可扩展的开源维基软件 使用 wiki.js 美丽直观的界面,让文档成为写作的乐趣! 优点 随时随地安装 几乎适用于任何平台,并与PostgreSQL.MySQL.Maria ...

  4. 标准正态分布表—R语言

    正态分布是最重要的一种概率分布.正态分布概念是由德国的数学家和天文学家Moivre于1733年首次提出的,但由于德国数学家Gauss率先将其应用于天文学家研究,故正态分布又叫高斯分布.高斯这项工作对后 ...

  5. pandas之读取文件

    当使用 Pandas 做数据分析的时,需要读取事先准备好的数据集,这是做数据分析的第一步.Panda 提供了多种读取数据的方法: read_csv() 用于读取文本文件 read_json() 用于读 ...

  6. [Linux]mysql错误总结-ERROR 1067 (42000): Invalid default value for TIMESTAMP

    MySQL的TIMESTAMP类型的默认值设置无效. 0 使用环境描述 Linux CentOS 7.8.2003 x86/64bit MySQL: 5.7.24 (mysql --version / ...

  7. SprintBoot2报错汇总

    报错1:SpringBoot找不到bean Unable to start ServletWebServerApplicationContext due to missing ServletWebSe ...

  8. ASP.NET Core - 缓存之内存缓存(下)

    话接上篇 [ASP.NET Core - 缓存之内存缓存(上)],所以这里的目录从 2.4 开始. 2.4 MemoryCacheEntryOptions MemoryCacheEntryOption ...

  9. python:调用内置函数

    问题描述:尝试下博客园如何上传GIF # hzh 每天进步一点点 # 2022/5/13 17:24 import colorama import time import os colorama.in ...

  10. Vue路由实现的底层原理

    在Vue中利用数据劫持defineProperty在原型prototype上初始化了一些getter,分别是router代表当前Router的实例 . router代表当前Router的实例.rout ...