iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
自我挑戰組

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

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

  • 分享至 

  • xImage
  •  

前言

今天我們來看看, 當創建一個 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
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言