最近入職了新公司,嘗試閱讀祖傳程式碼,記錄並更新最近的程式設計認知。
思緒由Q1引發,後續Q2、Q3基於Q1的發散探究
Q1. Task.Run、Task.Factory.StartNew 的區別?
我們常使用
Task.Run
和Task.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);
文末總結
- Task.Factory.StartNew 精細化控制,Task.Run 是特例
- 執行緒池任務調取器 對長時間執行的任務 做了兜底方案
- 自定義任務排程器