首页 > 其他分享 >揭秘 Task.Wait

揭秘 Task.Wait

时间:2023-06-15 10:03:13浏览次数:47  
标签:cancellationToken task Task 线程 millisecondsTimeout 揭秘 Wait

 

目录

 

简介

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 中的线程,会立即创建新的线程,避免了线程池中的可用线程数量不足的问题。但是这也会导致线程频繁的创建和销毁,导致性能下降。

欢迎关注个人技术公众号

 

 

出处:https://www.cnblogs.com/eventhorizon/p/17481757.html

标签:cancellationToken,task,Task,线程,millisecondsTimeout,揭秘,Wait
From: https://www.cnblogs.com/mq0036/p/17482048.html

相关文章

  • 理解linux的IOWait
    看到许多Linux性能工程师将CPU使用的"IOWait"部分视为系统何时处于I/O瓶颈的标识。本文将解释为什么这种方法是不可靠的,以及你可以使用哪些更好的指标。从运行一个小实验开始——在系统上产生大量的I/O使用:sysbench--threads=8--time=0--max-requests=0fileio--file-nu......
  • ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效(oracle 锁表)(转载
    1、查看数据库内产生了哪些锁selectt2.username,t2.sid,t2.serial#,t2.logon_timefromv$locked_objectt1,v$sessiont2wheret1.session_id=t2.sidorderbyt2.logon_time;如:   USERNAMESIDSERIAL#LOGON_TIMElurou851241832013/7/3011:44:45知道被锁的用户l......
  • pytest 执行脚本时,报(no name '/Users/**/PycharmProjects/interface_auto/test_case/
    触发场景:pytest执行脚本时,命名全部正确,但是直接报找不到执行函数解决方式:取掉init方法原因:测试框架在运行测试时会自动实例化测试类的对象,并且不会传递任何参数。如果您定义了__init__方法,测试框架将无法实例化您的测试类,从而导致测试无法运行。因此,为了确保测试类能够正......
  • 京东云端到端多媒体关键技术揭秘
    从带来更高编码效率、更好的用户体验的京享高清,到直播架构与网络演进优化,从而为用户带来更流畅的观看体验,以及运维系统的异常自动修复和高弹性的多媒体存储架构,一层一层展示出复杂而有序的多媒体技术框架。本文是对在LiveVideoStackCon2019北京的京东云技术专场的......
  • ASP.NET Core 6框架揭秘实例演示[38]:两种不同的限流策略
    承载ASP.NET应用的服务器资源总是有限的,短时间内涌入过多的请求可能会瞬间耗尽可用资源并导致宕机。为了解决这个问题,我们需要在服务端设置一个阀门将并发处理的请求数量限制在一个可控的范围,即使会导致请求的延迟响应,在极端的情况会还不得不放弃一些请求。ASP.NET应用的流量限制......
  • axios-结合async和await调用axios
    <!DOCTYPEhtml><html><head><metacharset="utf-8"><title></title></head><body><buttonid="btnPost">发起POST请求</button><scriptsrc="lib/axios.js">&......
  • celery笔记三之task和task的调用
    本文首发于公众号:Hunter后端原文链接:celery笔记三之task和task的调用这一篇笔记介绍task和task的调用。以下是本篇笔记目录:基础的task定义方式日志处理任务重试忽略任务运行结果task的调用1、基础的task定义方式前面两篇笔记中介绍了最简单的定义方式,使用......
  • 字节跳动双11电商直播技术大揭秘
    近几年来,电商直播已经成为了双11促销活动中的重要形式。作为国内电商直播中的佼佼者,双11刚过,抖音便公布了“抖音双11好物节数据报告”。从报告来看,10月27日至11月11日,抖音电商直播间累计时长达2546万小时,直播间累计观看395亿次,消费者品质购物需求旺盛,老字号产品、地方农货、非遗手......
  • 工作时间缩短神器,你用了吗?AI助手独家揭秘!
    像ChatGPT、Midjourney和Tome这样的新工具帮助专业人士节省时间并增加他们的收入。有些人说生成式人工智能将改变劳动力,但是它已经产生了影响。许多工人,特别是自由职业者和小企业主,为了摆脱了大公司的法律障碍,已经开始使用生成式人工智能工具来节省时间。他们说:新技术,包括图像和文......
  • promise、async、await
    一、promise语法上:promise是一个对象,从它可以获取异步操作的消息本意上:它是承诺,承诺它过一段时间会给你一个结果【如果想通过异步方法先后检测用户名和密码,需要先异步检测用户名,然后再异步检测密码的情况下就很适合Promise】1、创建此构造函数包含一个参数和一个带有resolv......