面試八股文:你寫過自定義任務排程器嗎?

_小碼甲發表於2021-05-06

最近入職了新公司,嘗試閱讀祖傳程式碼,記錄並更新最近的程式設計認知。

思緒由Q1引發,後續Q2、Q3基於Q1的發散探究

Q1. Task.Run、Task.Factory.StartNew 的區別?

我們常使用Task.RunTask.Factory.StartNew建立並啟動任務,但是他們的區別在哪裡?在哪種場景下使用前後者?

官方推薦使用Task.Run方法啟動基於計算的任務,
當需要對長時間執行、基於計算的任務做精細化控制時使用Task.Factory.StartNew。

Task.Factory提供了自定義選項、自定義排程器的能力,這也說明Task.Run是Task.Factory.StartNew的一個特例,Task.Run 只是提供了一個無參、預設的任務建立和排程方式。

當你在Task.Run傳遞委託

Task.Run(someAction);

實際上等價於

Task.Factory.StartNew(someAction, 
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

一個長時間執行的任務,如果使用Task.Run特定會使用執行緒池執行緒,可能構成濫用執行緒池執行緒,這個時候最好在獨立執行緒中執行任務。

Q2. 既然說到Task.Run使用執行緒池執行緒,執行緒池執行緒有哪些特徵? 為什麼有自定義排程器一說?

github: TaskScheduler 251行顯示TaskSchedule.Dafult確實是執行緒池任務排程器。

執行緒池執行緒的特徵:
① 池中執行緒都是後臺執行緒
② 執行緒可重用,一旦執行緒池中的執行緒完成任務,將返回到等待執行緒佇列中, 避免了建立執行緒的開銷
③ 池中預熱了工作者執行緒、IO執行緒,

  • 執行緒池最大執行緒數:執行緒池執行緒都忙碌,後續任務將排隊等待空閒執行緒;
  • 最小值:執行緒池根據需要提供 工作執行緒/IO完成執行緒, 直到達到某最小值; 達到某最小值,執行緒池可以建立或者等待。

我啟動一個腳手架專案: 預設最大工作者執行緒32767,最大IO執行緒1000 ; 預設最小工作執行緒數、最小IO執行緒數均為8個

github: ThreadPoolTaskScheduler 顯示執行緒池任務排程器是這樣排程任務的:

/// <summary>
/// Schedules a task to the ThreadPool.
/// </summary>
/// <param name="task">The task to schedule.</param>
protected internal override void QueueTask(Task task)
{
     TaskCreationOptions options = task.Options;
     if ((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 preferLocal = ((options & TaskCreationOptions.PreferFairness) == 0);
        ThreadPool.UnsafeQueueUserWorkItemInternal(task, preferLocal);
    }
}

注意8-14行:若上層使用者將LongRunning任務應用到預設的任務排程器(也即執行緒池任務排程器),執行緒池任務排程器會有一個兜底方案,會將任務放在獨立執行緒上執行。

何時不使用執行緒池執行緒

有幾種應用場景,其中適合建立並管理自己的執行緒,而非使用執行緒池執行緒:

  • 需要一個前臺執行緒。
  • 需要具有特定優先順序的執行緒。
  • 擁有會導致執行緒長時間阻塞的任務。 執行緒池具有最大執行緒數,因此大量被阻塞的執行緒池執行緒可能會阻止任務啟動。
  • 需將執行緒放入單執行緒單元。 所有 ThreadPool 執行緒均位於多執行緒單元中。
  • 需具有與執行緒關聯的穩定標識,或需將一個執行緒專用於一項任務。

Q3. 既然要自定義排程器,那我們就來自定義一下?

實現TaskScheduler 抽象類,其中的抓手是“排程”,也就是 QueueTask 方法,之後你自由定義資料結構, 從資料結構中排程出執行緒來執行任務。

public sealed class CustomTaskScheduler : TaskScheduler, IDisposable
    {
        private BlockingCollection<Task> tasksCollection = new BlockingCollection<Task>();
        private readonly Thread mainThread = null;
        public CustomTaskScheduler()
        {
            mainThread = new Thread(new ThreadStart(Execute));
            if (!mainThread.IsAlive)
            {
                mainThread.Start();
            }
        }
        private void Execute()
        {
            foreach (var task in tasksCollection.GetConsumingEnumerable())
            {
                TryExecuteTask(task);
            }
        }
        protected override IEnumerable<Task> GetScheduledTasks()
        {
            return tasksCollection.ToArray();
        }
        protected override void QueueTask(Task task)
        {
            if (task != null)
                tasksCollection.Add(task);           
        }
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            return false;
        }
        private void Dispose(bool disposing)
        {
            if (!disposing) return;
            tasksCollection.CompleteAdding();
            tasksCollection.Dispose();
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

應用我們的自定義任務排程器:

CustomTaskScheduler taskScheduler = new CustomTaskScheduler();
Task.Factory.StartNew(() => SomeMethod(), CancellationToken.None, TaskCreationOptions.None, taskScheduler);

文末總結

  1. Task.Factory.StartNew 精細化控制,Task.Run 是特例
  2. 執行緒池任務調取器 對長時間執行的任務 做了兜底方案
  3. 自定義任務排程器

相關文章