C#?TaskScheduler任務(wù)調(diào)度器的實現(xiàn)
什么是TaskScheduler?
SynchronizationContext是對“調(diào)度程序(scheduler)”的通用抽象。個別框架會有自己的抽象調(diào)度程序,比如System.Threading.Tasks。當(dāng)Tasks通過委托的形式進(jìn)行排隊和執(zhí)行時,會用到System.Threading.Tasks.TaskScheduler。和SynchronizationContext提供了一個virtual Post方法用于將委托排隊調(diào)用一樣(稍后,我們會通過典型的委托調(diào)用機(jī)制來調(diào)用委托),TaskScheduler也提供了一個abstract QueueTask方法(稍后,我們會通過ExecuteTask方法來調(diào)用該Task)。
通過TaskScheduler.Default我們可以獲取到Task默認(rèn)的調(diào)度程序ThreadPoolTaskScheduler——線程池(譯注:這下知道為什么Task默認(rèn)使用的是線程池線程了吧)。并且可以通過繼承TaskScheduler來重寫相關(guān)方法來實現(xiàn)在任意時間任意地點進(jìn)行Task調(diào)用。例如,核心庫中有個類,名為System.Threading.Tasks.ConcurrentExclusiveSchedulerPair,其實例公開了兩個TaskScheduler屬性,一個叫ExclusiveScheduler,另一個叫ConcurrentScheduler。調(diào)度給ConcurrentScheduler的任務(wù)可以并發(fā),但是要在構(gòu)造ConcurrentExclusiveSchedulerPair時就要指定最大并發(fā)數(shù)(類似于前面演示的MaxConcurrencySynchronizationContext);相反,在ExclusiveScheduler執(zhí)行任務(wù)時,那么將只允許運行一個排他任務(wù),這個行為很像讀寫鎖。
和SynchronizationContext一樣,TaskScheduler也有一個Current屬性,會返回當(dāng)前調(diào)度程序。不過,和SynchronizationContext不同的是,它沒有設(shè)置當(dāng)前調(diào)度程序的方法,而是在啟動Task時就要提供,因為當(dāng)前調(diào)度程序是與當(dāng)前運行的Task相關(guān)聯(lián)的。所以,下方的示例程序會輸出“True”,這是因為和StartNew一起使用的lambda表達(dá)式是在ConcurrentExclusiveSchedulerPair的ExclusiveScheduler上執(zhí)行的(我們手動指定cesp.ExclusiveScheduler),并且TaskScheduler.Current也
using System; using System.Threading.Tasks; class Program { static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler) .Wait(); } }
TaskScheduler 任務(wù)調(diào)度器的原理
public abstract class TaskScheduler { // 任務(wù)入口,待調(diào)度執(zhí)行的 Task 會通過該方法傳入,調(diào)度器會將任務(wù)安排task到指定的隊列(線程池任務(wù)隊列(全局任務(wù)隊列、本地隊列)、獨立線程、ui線程) 只能被.NET Framework調(diào)用,不能配派生類調(diào)用 // protected internal abstract void QueueTask(Task task); // 這個是在執(zhí)行 Task 回調(diào)的時候才會被執(zhí)行到的方法,放到后面再講 protected abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);
protected abstract bool TryExecuteTask(Task task, bool taskWasPreviouslyQueued);
// 獲取所有調(diào)度到該 TaskScheduler 的 Task protected abstract IEnumerable<Task>? GetScheduledTasks(); }
.net中的任務(wù)調(diào)度器有哪些
線程池任務(wù)調(diào)度器:ThreadPoolTaskScheduler、
核心庫任務(wù)調(diào)度器:ConcurrentExclusiveSchedulerPair
UI任務(wù)調(diào)度器:SynchronizationContextTaskScheduler,并發(fā)度為1
平時我們在用多線程開發(fā)的時候少不了Task,確實task給我們帶來了巨大的編程效率,在Task底層有一個TaskScheduler,它決定了task該如何被調(diào)度,而在.net framework中有兩種系統(tǒng)定義Scheduler,第一個是Task默認(rèn)的ThreadPoolTaskScheduler,還是一種就是SynchronizationContextTaskScheduler(wpf),默認(rèn)的調(diào)度器無法控制任務(wù)優(yōu)先級,那么需要自定義調(diào)度器實現(xiàn)優(yōu)先級控制。以及這兩種類型之外的如何自定義,這篇剛好和大家分享一下。
一: ThreadPoolTaskScheduler
這種scheduler機(jī)制是task的默認(rèn)機(jī)制,而且從名字上也可以看到它是一種委托到ThreadPool的機(jī)制,剛好也從側(cè)面說明task是基于ThreadPool基礎(chǔ)上的封裝,源代碼
ThreadPoolTaskScheduler的原理:將指定的長任務(wù)開辟一個獨立的線程去執(zhí)行,未指定的長時間運行的任務(wù)就用線程池的線程執(zhí)行
internal sealed class ThreadPoolTaskScheduler : TaskScheduler { //其他代碼 protected internal override void QueueTask(Task task) { TaskCreationOptions options = task.Options; if (Thread.IsThreadStartSupported && (options & TaskCreationOptions.LongRunning) != 0) { // Run LongRunning tasks on their own dedicated thread. new Thread(s_longRunningThreadWork) { IsBackground = true, Name = ".NET Long Running Task" }.UnsafeStart(task); } else { // Normal handling for non-LongRunning tasks. ThreadPool.UnsafeQueueUserWorkItemInternal(task, (options & TaskCreationOptions.PreferFairness) == 0); } } //其他代碼 }
二:SynchronizationContextTaskScheduler
使用條件:只有當(dāng)前程的同步上下文不為null時,該方法才能正常使用。例如在UI線程(wpf、 winform、 asp.net)中,UI線程的同步上下文不為Null。控制臺默認(rèn)的當(dāng)前線程同步上下文為null,如果給當(dāng)前線程設(shè)置默認(rèn)的同步上下文SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());就可以正常使用該方法。如果控制臺程序的線程未設(shè)置同步上下將引發(fā)【當(dāng)前的 SynchronizationContext 不能用作 TaskScheduler】異常。
默認(rèn)的同步上下文將方法委托給線程池執(zhí)行。
使用方式:通過TaskScheduler.FromCurrentSynchronizationContext() 調(diào)用SynchronizationContextTaskScheduler。
原理:初始化時候捕獲當(dāng)前的線程的同步上下文。 將同步上下文封裝入任務(wù)調(diào)度器形成新的任務(wù)調(diào)度器SynchronizationContextTaskScheduler。重寫該任務(wù)調(diào)度器中的QueueTask方法,利用同步上下文的post方法將任務(wù)送到不同的處理程序,如果是winform的UI線程同步上下文 的post方法(已重寫post方法),就將任務(wù)送到UI線程。如果是控制臺線程(默認(rèn)為null 設(shè)置默認(rèn)同步上下文后可以正常使用。默認(rèn)同步上下文采用線程池線程)就將任務(wù)送入線程池處理。
在winform中的同步上下文:WindowsFormsSynchronizationContext
在wpf中的同步上下文:DispatcherSynchronizationContext
在控制臺\線程池\new thread 同步上下文:都默認(rèn)為Null。可以給他們設(shè)置默認(rèn)的同步上下文SynchronizationContext。SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
SynchronizationContext 綜述 | Microsoft Docs
以下是SynchronizationContextTaskScheduler部分源代碼
internal sealed class SynchronizationContextTaskScheduler : TaskScheduler { //初始化時候 ,捕獲當(dāng)前線程的同步上下文 internal SynchronizationContextTaskScheduler() { m_synchronizationContext = SynchronizationContext.Current ?? // make sure we have a synccontext to work with throw new InvalidOperationException(SR.TaskScheduler_FromCurrentSynchronizationContext_NoCurrent); } //其他代碼 private readonly SynchronizationContext m_synchronizationContext; protected internal override void QueueTask(Task task) { m_synchronizationContext.Post(s_postCallback, (object)task); } //其他代碼 ///改變post的調(diào)度方法、 調(diào)用者線程執(zhí)行各方面的任務(wù)操作 private static readonly SendOrPostCallback s_postCallback = static s => { Debug.Assert(s is Task); ((Task)s).ExecuteEntry(); //調(diào)用者線程執(zhí)行各方面的任務(wù)操作 }; }
以下是SynchronizationContext部分源代碼
public partial class SynchronizationContext { //其他代碼 public virtual void Post(SendOrPostCallback d, object? state) => ThreadPool.QueueUserWorkItem(static s => s.d(s.state), (d, state), preferLocal: false); //其他代碼 }
有了這個基礎(chǔ)我們再來看一下代碼怎么寫,可以看到,下面這段代碼是不阻塞UIThread的,完美~~~
private void button1_Click(object sender, EventArgs e) { Task task = Task.Factory.StartNew(() => { //復(fù)雜操作,等待10s Thread.Sleep(10000); }).ContinueWith((t) => { button1.Text = "hello world"; }, TaskScheduler.FromCurrentSynchronizationContext()); }
三:自定義TaskScheduler
我們知道在現(xiàn)有的.net framework中只有這么兩種TaskScheduler,有些同學(xué)可能想問,這些Scheduler我用起來不爽,我想自定義一下,這個可以嗎?當(dāng)然?。?!如果你想自定義,只要自定義一個類實現(xiàn)一下TaskScheduler就可以了,然后你可以將ThreadPoolTaskScheduler簡化一下,即我要求所有的Task都需要走Thread,杜絕使用TheadPool,這樣可以嗎,當(dāng)然了,不信你看。
namespace ConsoleApplication1 { class Program { static void Main(string[] args) { var task = Task.Factory.StartNew(() => { Console.WriteLine("hello world?。?!"); }, new CancellationToken(), TaskCreationOptions.None, new PerThreadTaskScheduler()); Console.Read(); } } /// <summary> /// 每個Task一個Thread /// </summary> public class PerThreadTaskScheduler : TaskScheduler { protected override IEnumerable<Task> GetScheduledTasks() { return null; } protected override void QueueTask(Task task) { var thread = new Thread(() => { TryExecuteTask(task); }); thread.Start(); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { throw new NotImplementedException(); } } }
創(chuàng)建一個與當(dāng)前SynchronizationContext關(guān)聯(lián)的TaskScheduler。源代碼如下:
假設(shè)有一個UI App,它有一個按鈕。當(dāng)點擊按鈕后,會從網(wǎng)上下載一些文本并將其設(shè)置為按鈕的內(nèi)容。我們應(yīng)當(dāng)只在UI線程中訪問該按鈕,因此當(dāng)我們成功下載新的文本后,我們需要從擁有按鈕控制權(quán)的的線程中將其設(shè)置為按鈕的內(nèi)容。如果不這樣做的話,會得到一個這樣的異常:
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
如果我們自己手動實現(xiàn),那么可以使用前面所述的SynchronizationContext
將按鈕內(nèi)容的設(shè)置傳回原始上下文,例如借助TaskScheduler
:
用法如下
private static readonly HttpClient s_httpClient = new HttpClient(); private void downloadBtn_Click(object sender, RoutedEventArgs e) { s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask => { downloadBtn.Content = downloadTask.Result; }, TaskScheduler.FromCurrentSynchronizationContext());//捕獲當(dāng)前UI線程的同步上下文 }
到此這篇關(guān)于C# TaskScheduler任務(wù)調(diào)度器的實現(xiàn)的文章就介紹到這了,更多相關(guān)C# TaskScheduler任務(wù)調(diào)度器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Unity3D實現(xiàn)待機(jī)狀態(tài)圖片循環(huán)淡入淡出
這篇文章主要為大家詳細(xì)介紹了Unity3D實現(xiàn)待機(jī)狀態(tài)圖片循環(huán)淡入淡出,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-04-04