[Day 6] .Net WhenAll 底层(1)

前言

这系列教学文的目的是要探索具备非同步功能的框架在底层发生了什麽事, 甚至写一个简单的框架出来, 目前我们终於进展到阅读我们的第一个目标 .Net 。 之所以选择 .Net 作为开始, 一是因为我 C# 比较熟, 二是因为他们拥有很好的文件和索引, 三是因为他们的封装逻辑很棒(个人感受), 那我们就一起来看看吧。

先备知识

API 文件

https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.tasks.task.whenall?view=net-5.0

怕大家不懂 C# , 这边说明一下, Task.WhenAll 的功能和 JS 的 Promise.all 差不多。

说白了就是停下来等待一堆非同步工作的执行。

基本用法

要先包装出多个非同步工作, 接着把这些工作当参数传入 WhenAll, 最後 WhenAll 会回传执行结果。这个方法应该是 C# 使用端在实践非同步 programming 得常见做法了吧。

本篇文章参考

.Net reference source

https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,5955

先看注解

///
/// Creates a task that will complete when all of the supplied tasks have completed.
///
/// The tasks to wait on for completion.
/// A task that represents the completion of all of the supplied tasks.
///
///
/// If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state,
/// where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
///
///
/// If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
///
///
/// If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state.
///
///
/// If the supplied array/enumerable contains no tasks, the returned task will immediately transition to a RanToCompletion
/// state before it's returned to the caller.
///

可以知道

  • 总结 : 创建一个将在所有提供的任务完成後完成的任务。
  • 参数 : 已被包装好等待完成的非同步任务列表
  • 回传 : 任务列表完成的状态

看 source code

我们要读的是这个 overload public static Task WhenAll(params Task[] tasks)

public static Task WhenAll(params Task[] tasks)
{
    // Do some argument checking and make a defensive copy of the tasks array
    if (tasks == null) throw new ArgumentNullException("tasks");
    Contract.EndContractBlock();

    int taskCount = tasks.Length;
    if (taskCount == 0) return InternalWhenAll(tasks); // Small optimization in the case of an empty array.
    Task[] tasksCopy = new Task[taskCount];
    for (int i = 0; i < taskCount; i++)
    {
        Task task = tasks[i];
        if (task == null) throw new ArgumentException(Environment.GetResourceString("Task_MultiTaskContinuation_NullTask"), "tasks");
        tasksCopy[i] = task;
    }
    // 以上例外处理, 跳过。
    // The rest can be delegated to InternalWhenAll()
    return InternalWhenAll(tasksCopy);
}

可以发现, 其复制了一份传入参数, 丢入 InternalWhenAll(tasksCopy) , 代其回传。

接着我们读一下 InternalWhenAll(tasksCopy)

private static Task InternalWhenAll(Task[] tasks)
{
    Contract.Requires(tasks != null, "Expected a non-null tasks array");
    return (tasks.Length == 0) ? // take shortcut if there are no tasks upon which to wait
        Task.CompletedTask :
        new WhenAllPromise(tasks);
}

发现他除了例外处理以外, 又往下丢一层, 进入 WhenAllPromise(tasks)

internal WhenAllPromise(Task[] tasks) : base()
{
    Contract.Requires(tasks != null, "Expected a non-null task array");
    Contract.Requires(tasks.Length > 0, "Expected a non-zero length task array");

    if (AsyncCausalityTracer.LoggingOn)
        AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Id, "Task.WhenAll", 0);

    if (s_asyncDebuggingEnabled)
    {
        AddToActiveTasks(this);
    }

    m_tasks = tasks;
    // 记下任务总数
    m_count = tasks.Length;

    foreach (var task in tasks)
    {
        // 当这个任务被判断为已经完成, 则走捷径
        if (task.IsCompleted) this.Invoke(task); // short-circuit the completion action, if possible
        else task.AddCompletionAction(this); // simple completion action
    }
}

一般来说要看一下继承, 但 base() 是空的, 跳过。

跳过 log 相关, debug 相关, 我们可以发现两条核心方法, this.Invoke(task)task.AddCompletionAction(this) 两条, 但从注解可以看出, 走InvokeAddCompletionAction 的捷径, 我们先读Invoke。 这里的 this 指的是等待所有任务完成的任务, 就是本 method 的调用者。

public void Invoke(Task completedTask)

https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,6123

较长, 可以直接到 reference 看, 这里撷取语句

// atomic 的把 task 的数量减一, 如果减完是 0 表示任务全部完成
if (Interlocked.Decrement(refm_count) == 0)
{
    // 设定回传结果, 略
}

我们整理一下 当 whenAll 开始运行, 会先取得传入的 task 总数, 接着依序对传入的 task 判断状态, 若是判断为 task 已完成时, 会把 task 总数减1, 当总数被减为 0 时, 会设定回传结果且回传。

Interlocked.Decrement 文件

https://docs.microsoft.com/zh-tw/dotnet/api/system.threading.interlocked.decrement?view=net-5.0

明天进度

今天我们留下了两个疑问, 要交给明天的我解决。

  1. 为何要 "atomic" 的把 task 的总数减一, 看起来只有一条 thread 会动到啊 ?
  2. 如果 WhenAll 在查看 task 状态时, task 未完成会进入另一条路线, 那条路线发生了什麽呢 ?

明天见!


<<:  [Day 6] 蒐集对话经验

>>:  [Day05] Tableau 轻松学 - Tableau 授权类型

DAY12-JAVA的类别(6)-变数和函数

实例变数 实例变数(instance variable)拥有储存资料成员的记忆体空间,不与其他物件共...

企业资料通讯Week5 (2) | electronic mail [SMTP部分]

electronic mail 三要件 1. user agents(UA) 邮件使用者代理人,也叫...

Day 6 : HTML - 网页排版超强神器part_2,CSS grid到底是什麽?

上篇介绍了CSS Flex,这篇想来聊聊CSS grid到底是什麽东西 这里想先给大家一个观念: F...

Qualcomm announces next-gen X65 5G modem

Qualcomm has recently announced its upcoming 5G mo...

[DAY22] Boxenn Use Case Spec

Use Case Spec 这边以之前的 use case 当作例子来撰写测试。 首先要能快速地建立...