{C#} How task works?

weixin_34402408發表於2016-08-17

.net 從4.5開始對 Task 有了良好的支援。可以很方便的建立任務:

Task.Factory.StartNew(func<T>);

內部邏輯類似於:

var task = new Task<T>(func, ...);
task.ScheduleAndStart(...);

那麼當我們寫下這行程式碼時,究竟發生了什麼?
首先,任務在建立時可以指定任務排程器,如果不提供將採用預設的執行緒池排程器(ThreadPoolTaskScheduler)。當 task 啟動的時候,會把自身投遞到工作管理員中。

m_taskScheduler.InternalQueueTask(this);

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

    task.FireTaskScheduledIfNeeded(this);

    this.QueueTask(task);
}

接著再看 ThreadPoolTaskScheduler.QueueTask(Task):

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);
    }
}

ThreadPoolTaskScheduler 其實是對執行緒池的封裝。不過,對於【暗示將要】長時間執行的任務,為避免執行緒池阻塞,將直接建立新的執行緒。

ThreadPool.UnsafeQueueCustomWorkItem:

internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal)
{
    Contract.Assert(null != workItem);
    EnsureVMInitialized();

    //
    // Enqueue needs to be protected from ThreadAbort
    //
    try { }
    finally
    {
        ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
    }
}

很簡單,就是將任務加入到任務佇列中。注意 Task,Task<T>,QueueUserWorkItemCallback 都實現了 IThreadPoolWorkItem 介面。

ThreadPoolGlobals.workQueue.Enqueue:

public void Enqueue(IThreadPoolWorkItem callback, bool forceGlobal)
{
    ThreadPoolWorkQueueThreadLocals tl = null;
    if (!forceGlobal)
        tl = ThreadPoolWorkQueueThreadLocals.threadLocals;

    if (loggingEnabled)
        System.Diagnostics.Tracing.FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject(callback);
    
    if (null != tl)
    {
        tl.workStealingQueue.LocalPush(callback);
    }
    else
    {
        QueueSegment head = queueHead;

        while (!head.TryEnqueue(callback))
        {
            Interlocked.CompareExchange(ref head.Next, new QueueSegment(), null);

            while (head.Next != null)
            {
                Interlocked.CompareExchange(ref queueHead, head.Next, head);
                head = queueHead;
            }
        }
    }

    EnsureThreadRequested();
}

注意這段程式碼裡的細節。

  1. 如果 Task 是一個 Top Task(從主執行緒或者其他執行緒建立的),ThreadPoolWorkQueueThreadLocals(ThreadStatic) 不會被初始化,Task 將
    直接投遞到全域性佇列中。
  2. 如果 TaskB 是在 TaskA 中建立的,TaskB 會被插入到 TaskA 當前執行緒的工作佇列。因為在 TaskA 的當前執行緒中,ThreadPoolWorkQueueThreadLocals 已經建立(後面會講到為什麼)。
  3. EnsureThreadRequested 會請求必要的執行緒(如果沒有超過最大可用執行緒)。具體建立執行緒的程式碼不可見。

插入佇列的過程就到這裡結束了。 可是,,我們的task什麼時候能跑起來?
這就涉及到另一方面,執行緒是怎麼從佇列裡獲取任務的。執行緒大概會執行ThreadPool.Dispatch,在這個方法中首先會建立自身的佇列,然後獲取任務並執行。
獲取任務的演算法比較有意思,這裡貼出重要的部分。
ThreadPoolGlobals.workQueue.Dequeue(tl,...);

public void Dequeue(ThreadPoolWorkQueueThreadLocals tl, out IThreadPoolWorkItem callback, out bool missedSteal)
{
    callback = null;
    missedSteal = false;
    WorkStealingQueue wsq = tl.workStealingQueue;

    if (wsq.LocalPop(out callback))
        Contract.Assert(null != callback);

    if (null == callback)
    {
        QueueSegment tail = queueTail;
        while (true)
        {
            if (tail.TryDequeue(out callback))
            {
                Contract.Assert(null != callback);
                break;
            }

            if (null == tail.Next || !tail.IsUsedUp())
            {
                break;
            }
            else
            {
                Interlocked.CompareExchange(ref queueTail, tail.Next, tail);
                tail = queueTail;
            }
        }
    }

    if (null == callback)
    {
        WorkStealingQueue[] otherQueues = allThreadQueues.Current;
        int i = tl.random.Next(otherQueues.Length);
        int c = otherQueues.Length;
        while (c > 0)
        {
            WorkStealingQueue otherQueue = Volatile.Read(ref otherQueues[i % otherQueues.Length]);
            if (otherQueue != null &&
                otherQueue != wsq &&
                otherQueue.TrySteal(out callback, ref missedSteal))
            {
                Contract.Assert(null != callback);
                break;
            }
            i++;
            c--;
        }
    }
}

執行緒先從本身佇列裡獲取任務,然後從全域性佇列獲取,最後從其他執行緒的佇列裡偷任務。

值得注意的是,全域性佇列是FIFO,本地佇列是LIFO。兩者都採用了輕量鎖機制,尤其是全域性佇列,設計得非常巧妙。

接下來說說TaskA中建立TaskB的情況。TaskB在建立時如果沒有特別指定TaskScheduler,將使用TaskA的TaskScheduler。記住,是在TaskA執行中的執行緒啟動TaskB,而此執行緒在分發之時就已經建立了本地佇列,根據ThreadPoolGlobals.workQueue.Enqueue演算法,可以知道TaskB會被插入到本地佇列。所以,你應該知道如何利用Task這一特性來有針對性的建立和啟動task。

相關文章