3

揭秘 Task.Wait

 1 year ago
source link: https://www.cnblogs.com/eventhorizon/p/17481757.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

欢迎关注个人技术公众号

1201123-20230302194546214-138980196.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK