簡(jiǎn)介
Task.Wait 是 Task 的一個(gè)實(shí)例方法,用于等待 Task 完成,如果 Task 未完成,會(huì)阻塞當(dāng)前線程。
非必要情況下,不建議使用 Task.Wait,而應(yīng)該使用 await。
本文將基于 .NET 6 的源碼來(lái)分析 Task.Wait 的實(shí)現(xiàn),其他版本的實(shí)現(xiàn)也是類(lèi)似的。
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 阻塞了當(dāng)前線程,直到 task 完成。
其效果等效于:
task.Result (僅限于 Task<TResult>)
task.GetAwaiter().GetResult()
task.Wait 共有 5 個(gè)重載
public class Task<TResult> : Task
{
}
public class Task
{
// 1. 無(wú)參數(shù),無(wú)返回值,阻塞當(dāng)前線程至 task 完成
public void Wait()
{
Wait(Timeout.Infinite, default);
}
// 2. 無(wú)參數(shù),有返回值,阻塞當(dāng)前線程至 task 完成或 超時(shí)
// 如果超時(shí)后 task 仍未完成,返回 False,否則返回 True
public bool Wait(TimeSpan timeout)
{
return Wait((int)timeout.TotalMilliseconds, default);
}
// 3. 和 2 一樣,只是參數(shù)類(lèi)型不同
public bool Wait(int millisecondsTimeout)
{
return Wait(millisecondsTimeout, default);
}
// 4. 無(wú)參數(shù),無(wú)返回值,阻塞當(dāng)前線程至 task 完成或 cancellationToken 被取消
// cancellationToken 被取消時(shí)拋出 OperationCanceledException
public void Wait(CancellationToken cancellationToken)
{
Wait(Timeout.Infinite, cancellationToken);
}
// 5. 無(wú)參數(shù),有返回值,阻塞當(dāng)前線程至 task 完成或 超時(shí) 或 cancellationToken 被取消
// 如果超時(shí)后 task 仍未完成,返回 False,否則返回 True
// cancellationToken 被取消時(shí)拋出 OperationCanceledException
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
{
ThrowIfContinuationIsNotNull();
return InternalWaitCore(millisecondsTimeout, cancellationToken);
}
}
下面是一個(gè)使用 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
因?yàn)橹付ǖ?millisecondsTimeout 不足以等待 task 完成,所以 task.Wait 返回 False,繼續(xù)執(zhí)行后續(xù)代碼。
但是,task.Result 仍然會(huì)阻塞當(dāng)前線程,直到 task 完成。
關(guān)聯(lián)的方法還有 Task.WaitAll 和 Task.WaitAny。同樣也是非必要情況下,不建議使用。
背后的實(shí)現(xiàn)
task.Wait、task.Result、task.GetAwaiter().GetResult() 這三者背后的實(shí)現(xiàn)其實(shí)是一樣的,都是調(diào)用了 Task.InternalWaitCore 這個(gè)實(shí)例方法。
借助 Rider 的類(lèi)庫(kù) debug 功能,來(lái)給大家展示一下這三種方法的調(diào)用棧。
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 的一個(gè)私有實(shí)例方法。
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 已經(jīng)完成,直接返回 true
bool returnValue = IsCompleted;
if (returnValue)
{
return true;
}
// 如果調(diào)用的是 Task.Wait 的無(wú)參重載方法,且Task 已經(jīng)完成或者在內(nèi)聯(lián)執(zhí)行后完成,直接返回 true,不會(huì)阻塞 Task.Wait 的調(diào)用線程。
// WrappedTryRunInline 的意思是嘗試在捕獲的 TaskScheduler 中以?xún)?nèi)聯(lián)的方式執(zhí)行 Task,此處不展開(kāi)
if (millisecondsTimeout == Timeout.Infinite && !cancellationToken.CanBeCanceled &&
WrappedTryRunInline() && IsCompleted)
{
returnValue = true;
}
else
{
// Task 未完成,調(diào)用 SpinThenBlockingWait 方法,阻塞當(dāng)前線程,直到 Task 完成或超時(shí)或 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,當(dāng) Task 完成時(shí),會(huì)調(diào)用 mres.Set() 方法
AddCompletionAction(mres, addBeforeOthers: true);
if (infiniteWait)
{
bool notifyWhenUnblocked = ThreadPool.NotifyThreadBlocked();
try
{
// 沒(méi)有指定超時(shí)時(shí)間,阻塞當(dāng)前線程,直到 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
{
// 指定了超時(shí)時(shí)間,阻塞當(dāng)前線程,直到 Task 完成或 超時(shí) 或 cancellationToken 被取消
returnValue = mres.Wait((int)(millisecondsTimeout - elapsedTimeTicks), cancellationToken);
}
finally
{
if (notifyWhenUnblocked)
{
ThreadPool.NotifyThreadUnblocked();
}
}
}
}
}
finally
{
// 如果因?yàn)槌瑫r(shí)或 cancellationToken 被取消,而導(dǎo)致 Task 未完成,需要將 mres 從 Task 的 Continuation 中移除
if (!IsCompleted) RemoveContinuation(mres);
}
}
return returnValue;
}
private bool SpinWait(int millisecondsTimeout)
{
if (IsCompleted) return true;
if (millisecondsTimeout == 0)
{
// 如果指定了超時(shí)時(shí)間為 0,直接返回 false
return false;
}
// 自旋至少一次,總次數(shù)由 Threading.SpinWait.SpinCountforSpinBeforeWait 決定
// 如果 Task 在自旋期間完成,返回 true
int spinCount = Threading.SpinWait.SpinCountforSpinBeforeWait;
SpinWait spinner = default;
while (spinner.Count < spinCount)
{
// -1 表示自旋期間不休眠,不會(huì)讓出 CPU 時(shí)間片
spinner.SpinOnce(sleep1Threshold: -1);
if (IsCompleted)
{
return true;
}
}
// 自旋結(jié)束后,如果 Task 仍然未完成,返回 false
return false;
}
private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
{
// 往父類(lèi) ManualResetEventSlim 中傳入 false,表示 ManualResetEventSlim 的初始狀態(tài)為 nonsignaled
// 也就是說(shuō),在調(diào)用 ManualResetEventSlim.Set() 方法之前,ManualResetEventSlim.Wait() 方法會(huì)阻塞當(dāng)前線程
internal SetOnInvokeMres() : base(false, 0) { }
public void Invoke(Task completingTask) { Set(); }
public bool InvokeMayRunArbitraryCode => false;
}
}
Task.Wait 的兩個(gè)階段
SpinWait 階段
用戶(hù)態(tài)鎖,不能維持很長(zhǎng)時(shí)間的等待。線程在等待鎖的釋放時(shí)忙等待,不會(huì)進(jìn)入休眠狀態(tài),從而避免了線程切換的開(kāi)銷(xiāo)。它在自旋等待期間會(huì)持續(xù)占用CPU時(shí)間片,如果自旋等待時(shí)間過(guò)長(zhǎng),會(huì)浪費(fèi)CPU資源。
BlockingWait 階段
內(nèi)核態(tài)鎖,在內(nèi)核態(tài)實(shí)現(xiàn)的鎖機(jī)制。當(dāng)線程無(wú)法獲得鎖時(shí),會(huì)進(jìn)入內(nèi)核態(tài)并進(jìn)入休眠狀態(tài),將CPU資源讓給其他線程。線程在內(nèi)核態(tài)休眠期間不會(huì)占用CPU時(shí)間片,從而避免了持續(xù)的忙等待。當(dāng)鎖可用時(shí),內(nèi)核會(huì)喚醒休眠的線程并將其調(diào)度到CPU上執(zhí)行。
BlockingWait 階段 主要借助 SetOnInvokeMres 實(shí)現(xiàn), SetOnInvokeMres 繼承自 ManualResetEventSlim。
它會(huì)阻塞調(diào)用線程直到 Task 完成 或 超時(shí) 或 cancellationToken 被取消。
當(dāng)前線程,Task 完成時(shí),SetOnInvokeMres.Set() 方法會(huì)被當(dāng)做 Task 的回調(diào)被調(diào)用從而解除阻塞。
Task.Wait 可能會(huì)導(dǎo)致的問(wèn)題
到目前為止,我們已經(jīng)了解到 Task.Wait 阻塞當(dāng)前線程等待 Task 完成的原理,但是我們還是沒(méi)有回答最開(kāi)始的問(wèn)題:為什么不建議使用 Task.Wait。
可能會(huì)導(dǎo)致線程池饑餓
線程池饑餓是指線程池中的可用線程數(shù)量不足,無(wú)法執(zhí)行任務(wù)的現(xiàn)象。
在 ThreadPool 的設(shè)計(jì)中,如果已經(jīng)創(chuàng)建的線程達(dá)到了一定數(shù)量,就算有新的任務(wù)需要執(zhí)行,也不會(huì)立即創(chuàng)建新的線程(每 500ms 才會(huì)檢查一次是否需要?jiǎng)?chuàng)建新的線程)。
更詳細(xì)的介紹可以參考我的另一篇文章:https://www.cnblogs.com/eventhorizon/p/15316955.html#3-避免饑餓機(jī)制starvation-avoidance
如果我們?cè)谝粋€(gè) ThreadPool 線程中調(diào)用 Task.Wait,而 Task.Wait 又阻塞了這個(gè)線程,無(wú)法執(zhí)行其他任務(wù),這樣就會(huì)導(dǎo)致線程池中的可用線程數(shù)量不足,從而阻塞了任務(wù)的執(zhí)行。
可能會(huì)導(dǎo)致死鎖
除此之外 Task.Wait 也可能會(huì)導(dǎo)致死鎖,這里就不展開(kāi)了。具體可以參考:https://www.cnblogs.com/eventhorizon/p/15912383.html
.NET 6 對(duì) Task.Wait 的優(yōu)化
細(xì)心的同學(xué)會(huì)注意到 SpinThenBlockingWait 的 BlockingWait 階段,會(huì)調(diào)用 ThreadPool.NotifyThreadBlocked() 方法,這個(gè)方法會(huì)通知線程池當(dāng)前線程被阻塞了,新的線程會(huì)被立即創(chuàng)建出來(lái)。
但這也不代表 Task.Wait 就可以放心使用了,ThreadPool 中的線程被大量阻塞,就算借助 ThreadPool.NotifyThreadBlocked() 能讓新任務(wù)繼續(xù)執(zhí)行,但這會(huì)導(dǎo)致線程頻繁的創(chuàng)建和銷(xiāo)毀,導(dǎo)致性能下降。
總結(jié)
Task.Wait 對(duì)調(diào)用線程的阻塞分為兩個(gè)階段:SpinWait 階段 和 BlockingWait 階段。如果 Task 完成較快,就可以在性能較好的 SpinWait 階段完成等待。
濫用 Task.Wait 會(huì)導(dǎo)致線程池饑餓或死鎖。
.NET 6 對(duì) Task.Wait 進(jìn)行了優(yōu)化,如果 Task.Wait 阻塞了 ThreadPool 中的線程,會(huì)立即創(chuàng)建新的線程,避免了線程池中的可用線程數(shù)量不足的問(wèn)題。但是這也會(huì)導(dǎo)致線程頻繁的創(chuàng)建和銷(xiāo)毀,導(dǎo)致性能下降。
?轉(zhuǎn)自https://www.cnblogs.com/eventhorizon/p/17481757.html
該文章在 2025/8/8 9:25:27 編輯過(guò)