iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
自我挑戰組

從C到JS的同步非同步探索系列 第 13

[Day 13] .Net Task 底層(6)

前言

今天我們來看看, 當創建一個 task 物件, 設定完要做的行為已及排程方式後, 會調用兩個方法, 他們分別代表甚麼意思。

前情提要

以下方法在執行Task.Run打包新task 調用

internal static Task InternalStartNew(
            Task creatingTask, Delegate action, object state, CancellationToken cancellationToken, TaskScheduler scheduler,
            TaskCreationOptions options, InternalTaskOptions internalOptions, ref StackCrawlMark stackMark)
{
    // Validate arguments.
    if (scheduler == null)
    {
        throw new ArgumentNullException("scheduler");
    }
    Contract.EndContractBlock();

    // Create and schedule the task. This throws an InvalidOperationException if already shut down.
    // Here we add the InternalTaskOptions.QueuedByRuntime to the internalOptions, so that TaskConstructorCore can skip the cancellation token registration
    Task t = new Task(action, state, creatingTask, cancellationToken, options, internalOptions | InternalTaskOptions.QueuedByRuntime, scheduler);
    t.PossiblyCaptureContext(ref stackMark);

    t.ScheduleAndStart(false);
    return t;
}

我們要來看看這兩個方法

t.PossiblyCaptureContext(ref stackMark);
t.ScheduleAndStart(false);

PossiblyCaptureContext

internal void PossiblyCaptureContext(ref StackCrawlMark stackMark)
{
    Contract.Assert(m_contingentProperties == null || m_contingentProperties.m_capturedContext == null,
        "Captured an ExecutionContext when one was already captured.");

    // In the legacy .NET 3.5 build, we don't have the optimized overload of Capture()
    // available, so we call the parameterless overload.
#if PFX_LEGACY_3_5
            CapturedContext = ExecutionContext.Capture();
#else
    CapturedContext = ExecutionContext.Capture(
        ref stackMark,
        ExecutionContext.CaptureOptions.IgnoreSyncCtx | ExecutionContext.CaptureOptions.OptimizeDefaultCase);
#endif
}

可以發現重點是調用 ExecutionContext.Capture其底層是利用 Thread 中的 method 獲取當前所在 thread 的內容並且回傳。

所以變數 CapturedContext , 經過這個方法後裡面存了 當前 thread 裡面的資料, 待用。

換句話說, task 存下了當初創建他的 thread 的執行內容。

ScheduleAndStart

internal void ScheduleAndStart(bool needsProtection)
{
    Contract.Assert(m_taskScheduler != null, "expected a task scheduler to have been selected");
    Contract.Assert((m_stateFlags & TASK_STATE_STARTED) == 0, "task has already started");

    // 設定任務開始的標記
    if (needsProtection)
    {
        if (!MarkStarted())
        {
            // A cancel has snuck in before we could get started.  Quietly exit.
            return;
        }
    }
    else
    {
        m_stateFlags |= TASK_STATE_STARTED;
    }

    if (s_asyncDebuggingEnabled)
    {
        AddToActiveTasks(this);
    }

    if (AsyncCausalityTracer.LoggingOn && (Options & (TaskCreationOptions)InternalTaskOptions.ContinuationTask) == 0)
    {
        //For all other task than TaskContinuations we want to log. TaskContinuations log in their constructor
        AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Id, "Task: " + ((Delegate)m_action).Method.Name, 0);
    }

    try
    {
        // 把當前任務送入 Scheduler 的隊列中
        m_taskScheduler.InternalQueueTask(this);
    }
    catch (ThreadAbortException tae)
    {
        AddException(tae);
        FinishThreadAbortedTask(true, false);
    }
    catch (Exception e)
    {
        // The scheduler had a problem queueing this task.  Record the exception, leaving this task in
        // a Faulted state.
        TaskSchedulerException tse = new TaskSchedulerException(e);
        AddException(tse);
        Finish(false);

        // Now we need to mark ourselves as "handled" to avoid crashing the finalizer thread if we are called from StartNew()
        // or from the self replicating logic, because in both cases the exception is either propagated outside directly, or added
        // to an enclosing parent. However we won't do this for continuation tasks, because in that case we internally eat the exception
        // and therefore we need to make sure the user does later observe it explicitly or see it on the finalizer.

        if ((Options & (TaskCreationOptions)InternalTaskOptions.ContinuationTask) == 0)
        {
            // m_contingentProperties.m_exceptionsHolder *should* already exist after AddException()
            Contract.Assert(
                (m_contingentProperties != null) &&
                (m_contingentProperties.m_exceptionsHolder != null) &&
                (m_contingentProperties.m_exceptionsHolder.ContainsFaultList),
                    "Task.ScheduleAndStart(): Expected m_contingentProperties.m_exceptionsHolder to exist " +
                    "and to have faults recorded.");

            m_contingentProperties.m_exceptionsHolder.MarkAsHandled(false);
        }
        // re-throw the exception wrapped as a TaskSchedulerException.
        throw tse;
    }
}

可以發現重點在 m_taskScheduler.InternalQueueTask(this);

其把當前任務送入 Scheduler 的隊列中, 而我們之前說過, 這個 Scheduler 其實就是 TP

所以我們繼續往下看

internal void InternalQueueTask(Task task)
{
    Contract.Requires(task != null);

    task.FireTaskScheduledIfNeeded(this);

    this.QueueTask(task);
}

重點在 QueueTask

他其實是一個介面, 擔當了所有類型 Scheduler 推入任務的入口

而因為我們知道 Scheduler 是 TP , 所以直接看 TP 複寫的 QueueTask

protected internal override void QueueTask(Task task)
{
    if ((task.Options & TaskCreationOptions.LongRunning) != 0)
    {
        // Run LongRunning tasks on their own dedicated thread.
        Thread thread = new Thread(s_longRunningThreadWork);
        thread.IsBackground = true; // Keep this thread from blocking process shutdown
        thread.Start(task);
    }
    else
    {
        // Normal handling for non-LongRunning tasks.
        bool forceToGlobalQueue = ((task.Options & TaskCreationOptions.PreferFairness) != 0);
        ThreadPool.UnsafeQueueCustomWorkItem(task, forceToGlobalQueue);
    }
}

這裡可以發現兩種 case ,

  1. longRunning , 指工程比較浩大的任務, 會調用 Thread 的方法, 創建一條新 thread 來運行
  2. Normal , 可以看到之前聊過的 UnsafeQueueCustomWorkItem 在處理連續任務區有遇到, 可以把任務放入 TP 來執行。

此外在ScheduleAndStart可以看到在例外處理有

  • FinishThreadAbortedTask(true, false);
  • Finish

兩種 method , 他們是用來表示主要任務完成, 會觸發前面提到的連續任務區。

ScheduleAndStart 要求同步運行時

實際上, 在實作場合, 創建一個 Task 但卻希望他完成後別的 Task 才繼續走的的情況非常常見。

其底層就是在 ScheduleAndStart 中調用 Scheduler 時不是使用預設的 TP 作為 Scheduler 而是使用 SynchronizationContextTaskScheduler類別作為 Scheduler , 以下查看其複寫的InternalQueueTask

protected internal override void QueueTask(Task task)
{
    m_synchronizationContext.Post(s_postCallback, (object)task);
}

public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

可以看見其一樣是推入 TP 來執行, 但是調用了 WaitCallback 作為參數, 該物件我往下看過, 來自外部程式碼, 推測功能是與同樣來自外部的 TP 底層 Scheduler 溝通, 使外部程式碼操作的 workerThread 用同步方式運行這個 TP 中的任務。

明天進度

到此, .Net Task 算是看得差不多了, 我明天會進行這幾天文章的整理, 算是做一個懶人包給大家。

明天見 !


上一篇
[Day12] .Net Task 底層(5)
下一篇
[Day 14] .Net 非同步概念整理
系列文
從C到JS的同步非同步探索30

尚未有邦友留言

立即登入留言