簡介
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;
}
}
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
.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 中的線程,會立即創建新的線程,避免了線程池中的可用線程數量不足的問題。但是這也會導致線程頻繁的創建和銷毀,導致性能下降。
?轉自https://www.cnblogs.com/eventhorizon/p/17481757.html
該文章在 2025/8/8 9:25:27 編輯過