LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

C# 揭秘 Task.Wait

freeflydom
2025年8月8日 9:25 本文熱度 429

簡介

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

.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 中的線程,會立即創建新的線程,避免了線程池中的可用線程數量不足的問題。但是這也會導致線程頻繁的創建和銷毀,導致性能下降。

?轉自https://www.cnblogs.com/eventhorizon/p/17481757.html


該文章在 2025/8/8 9:25:27 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
一本大道久久a久久综合 | 亚洲电影天堂在线对白 | 亚洲五月天激情在线视频 | 中文在线中文国产精品一 | 中文字幕日本一区二区三区 | 色婷婷AⅤ一区二区三区 |