揭秘 Task.Wait

揭秘,task,wait · 浏览次数 : 2623

小编点评

# Task.Wait 阻塞当前线程等待 Task 完成的原理 Task.Wait 阻塞当前线程等待 Task 完成的原理,但我们还是没有回答最开始的问题:为什么不建议使用 Task.Wait。可能会导致线程池饥饿线程池饥饿。 **Task.Wait 的两个阶段:** 1. SpinWait 阶段 2. BlockingWait 阶段 **SpinWait 阶段:** * 用户态锁,不能保证线程在该阶段获得锁 * 线程等待 Task 完成 * 用户态锁释放,线程可获取锁 **BlockingWait 阶段:** *内核态锁,等待新的线程创建 * 线程等待 Task 完成 *内核态锁释放,新的线程可获取锁 **注意事项:** * Task.Wait 会阻塞当前线程,直到 Task 完成或超时或 cancellationToken 被取消 *滥用 Task.Wait 会导致线程池饥饿或死锁 * .NET 6 对 Task.Wait 进行了优化,如果 Task.Wait 阻塞了 ThreadPool 中的线程,会立即创建新的线程,避免了线程池中的可用线程数量不足的问题

正文

简介

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.Wait

Task.Result

Task.GetAwaiter.GetResult

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相似的内容:

揭秘 Task.Wait

Task.Wait 是 Task 的一个实例方法,用于等待 Task 完成,如果 Task 未完成,会阻塞当前线程。 非必要情况下,不建议使用 Task.Wait,而应该使用 await。 本文将基于 .NET 6 的源码来分析 Task.Wait 的实现,其他版本的实现也是类似的。

.NET周报 【6月第3期 2023-06-18】

## 国内文章 ### 揭秘 Task.Wait https://www.cnblogs.com/eventhorizon/p/17481757.html Task.Wait 是 Task 的一个实例方法,用于等待 Task 完成,如果 Task 未完成,会阻塞当前线程。**非必要情况下,不建议使用

.NET Task 揭秘(3)async 与 AsyncMethodBuilder

前言 本文为系列博客 什么是 Task Task 的回调执行与 await async 与 AsyncMethodBuilder(本文) 总结与常见误区(TODO) 上文我们学习了 await 这个语法糖背后的实现,了解了 await 这个关键词是如何去等待 Task 的完成并获取 Task 执行结

揭秘 .NET 中的 TimerQueue(上)

[TOC] # 前言 TimerQueue 是.NET中实现定时任务的核心组件,它是一个定时任务的管理器,负责存储和调度定时任务。它被用于实现很多 .NET 中的定时任务,比如 System.Threading.Timer、Task.Delay、CancellationTokenSource 等。

解锁网络无限可能:揭秘微软工程师力作——付费代理IP池深度改造与实战部署指南

"揭秘微软工程师力作:付费代理IP池深度改造,四大模块精讲,含实战部署指南。掌握高效、稳定代理IP资源,解锁网络无限可能。从筛选管理到安全加密,详细步骤助您快速搭建专属代理网络。尊享付费阅读,获取深度技术洞察与实践指导。"

揭秘In-Context Learning(ICL):大型语言模型如何通过上下文学习实现少样本高效推理[示例设计、ICL机制详解]

揭秘In-Context Learning(ICL):大型语言模型如何通过上下文学习实现少样本高效推理[示例设计、ICL机制详解]

揭秘华为如此多成功项目的产品关键——Charter模板

很多推行IPD(集成产品开发)体系的公司在正式研发产品前,需要开发Charter,以确保产品研发方向的正确。Charter,即项目任务书或商业计划书。Charter的呈现标志着产品规划阶段的完成,能为产品开发的投资评估和决策提供关键依据。 在IPD体系中,Charter的核心逻辑主要体现在两点:一是

揭秘网络安全攻防战:信息收集和密码破解的黑客技巧与防护策略

今天的学习重点是网络安全基础知识,包括信息收集和弱口令密码破解。在信息收集方面,我们学习了目录信息的收集方法,特别是如何解析路径信息。在密码破解方面,我们讨论了使用简单的弱口令破解方法。同时,我也介绍了一些有效的防范渗透的方法。

[转帖]申威-揭秘中国军方神秘全自主芯片!

http://www.ichyang.com/post/2358.html 相对于从诞生之初就处于舆论风口浪尖的“龙芯”,中国另一款走全自主道路的芯片“申威”,相比之下就低调得多。陆媒近日刊文试图揭秘这款由军方秘密开发的全自主芯片。 无论是传统纸媒还是网络媒体,“申威”的曝光率比起“龙芯”、“海思”

揭秘 .NET 中的 TimerQueue(下)

[TOC] # 前言 上文给大家介绍了 TimerQueue 的任务调度算法。 https://www.cnblogs.com/eventhorizon/p/17557821.html 这边做一个简单的复习。 TimerQueue 中的基本任务单元是 TimerQueueTimer,封装待执行的定时