Task承載的操作需要被調(diào)度才能被執(zhí)行,由于.NET默認(rèn)采用基于線程池的調(diào)度器,所以Task默認(rèn)在線程池線程中執(zhí)行。但是有的操作并不適合使用線程池,比如我們?cè)谝粋€(gè)ASP.NET Core應(yīng)用中承載了一些需要長(zhǎng)時(shí)間執(zhí)行的后臺(tái)操作,由于線程池被用來(lái)處理HTTP請(qǐng)求,如果這些后臺(tái)操作也使用線程池來(lái)調(diào)度,就會(huì)造成相互影響。在這種情況下,使用獨(dú)立的一個(gè)或者多個(gè)線程來(lái)執(zhí)行這些后臺(tái)操作可能是一個(gè)更好的選擇。
一、基于線程池的調(diào)度
二、TaskCreationOptions.LongRunning
三、換成異步操作呢?
四、換種寫(xiě)法呢?
五、調(diào)用Wait方法
六、自定義TaskScheduler
七、獨(dú)立線程池
一、基于線程池的調(diào)度
我們通過(guò)如下這個(gè)簡(jiǎn)單的程序來(lái)驗(yàn)證默認(rèn)基于線程池的Task調(diào)度。我們調(diào)用Task類型的靜態(tài)屬性Factory返回一個(gè)TaskFactory對(duì)象,并調(diào)用其StartNew方法啟動(dòng)一個(gè)Task對(duì)象,這個(gè)Task指向的Run方法會(huì)在一個(gè)循環(huán)中調(diào)用Do方法。Do方法使用自旋等待的方式模擬一段耗時(shí)2秒的操作,并在控制臺(tái)輸出當(dāng)前線程的IsThreadPoolThread屬性確定是否是線程池線程。
Task.Factory.StartNew(Run); Console.Read(); void Run() { while (true) { Do(); } } void Do() { var end = DateTime.UtcNow.AddSeconds(2); SpinWait.SpinUntil(() => DateTimeOffset.UtcNow > end); var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}"); }
通過(guò)如下所示的輸出結(jié)果,我們得到了答案:利用TaskFactory創(chuàng)建的Task在默認(rèn)情況下確實(shí)是通過(guò)線程池的形式被調(diào)度的。
二、TaskCreationOptions.LongRunning
很明顯,上述Run方法是一個(gè)需要永久執(zhí)行的LongRunning操作,并不適合使用線程池來(lái)執(zhí)行,實(shí)際上TaskFactory在設(shè)計(jì)的時(shí)候就考慮到了這一點(diǎn),我們利用它創(chuàng)建一個(gè)Task的時(shí)候可以指定對(duì)應(yīng)的TaskCreationOptions選項(xiàng),其中一個(gè)選項(xiàng)就是LongRuning。我們通過(guò)如下的方式修改了上面這段程序,在調(diào)用StartNew方法時(shí)指定了這個(gè)選項(xiàng)。
Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning); Console.Read(); void Run() { while (true) { Do(); } } void Do() { var end = DateTime.UtcNow.AddSeconds(2); SpinWait.SpinUntil(() => DateTimeOffset.UtcNow > end); var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}"); }
再次執(zhí)行我們的程序,就會(huì)通過(guò)如下的輸出結(jié)果看到Do方法將不會(huì)在線程池線程中執(zhí)行了。
三、換成異步操作呢?
由于LongRunning操作經(jīng)常會(huì)涉及IO操作,所以我們執(zhí)行方法經(jīng)常會(huì)寫(xiě)成異步的形式。如下所示的代碼中,我們將Do方法替換成DoAsync,將2秒的自旋等待替換成Task.Delay。由于DoAsync寫(xiě)成了異步的形式,Run也換成對(duì)應(yīng)的RunAsync。
Task.Factory.StartNew(RunAsync, TaskCreationOptions.LongRunning); Console.Read(); async Task RunAsync() { while (true) { await DoAsync(); } } async Task DoAsync() { await Task.Delay(2000); var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}"); }
再次啟動(dòng)程序后,我們發(fā)現(xiàn)又切換成了線程池調(diào)度了。為什么會(huì)這樣呢?其實(shí)很好理解,由于原來(lái)返回void的Run方法被替換成了返回Task的RunAsync,傳入StartNew方法表示執(zhí)行操作的委托類型從Action切換成了Func<Task>,雖然我們指定了LongRunning選項(xiàng),但是StartNew方法只是采用這種模式執(zhí)行Func<Task>這個(gè)委托對(duì)象而已,而這個(gè)委托在遇到await的時(shí)候就返回了。至于返回的Task對(duì)象,還是按照默認(rèn)的方式進(jìn)行調(diào)度執(zhí)行。
四、換種寫(xiě)法呢?
有人說(shuō),上面我們使用的是一個(gè)方法來(lái)表示作為參數(shù)的委托對(duì)象,如果我們按照如下的方式使用基于async/await的Lambda表達(dá)式呢?實(shí)際上這樣的Lambda表達(dá)式就是Func<Task>的另一種編程方式而已。
Task.Factory.StartNew(async () => { while (true) await DoAsync();}, TaskCreationOptions.LongRunning);
Console.Read();
async Task DoAsync()
{
await Task.Delay(2000);
var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}");
}
五、調(diào)用Wait方法
其實(shí)這個(gè)問(wèn)題很好解決,按照如下的方式將DoAsync方法換成同步形式的Do,將基于await的等待替換成針對(duì)Wait方法的調(diào)用就可以了。我想當(dāng)你接觸Task的時(shí)候,就有很多人不斷提醒你,謹(jǐn)慎使用Wait方法,因?yàn)樗鼤?huì)阻塞當(dāng)前線程。實(shí)際上對(duì)于我們的當(dāng)前的應(yīng)用場(chǎng)景,調(diào)用Wait方法才是正確的選擇,因?yàn)槲覀兊某踔跃褪鞘褂靡粋€(gè)獨(dú)立的線程以獨(dú)占的方式來(lái)執(zhí)行后臺(tái)操作。
Task.Factory.StartNew(() => { while (true) Do(); }, TaskCreationOptions.LongRunning); Console.Read(); void Do() { Task.Delay(2000).Wait(); var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}"); }
六、自定義TaskScheduler
既然針對(duì)線程池的使用是“Task調(diào)度”導(dǎo)致的,我們自然可以通過(guò)重寫(xiě)TaskScheduler的方式來(lái)解決這個(gè)問(wèn)題。如下這個(gè)自定義的DedicatedThreadTaskScheduler 會(huì)采用獨(dú)立的線程來(lái)執(zhí)行被調(diào)度的Task,線程的數(shù)量可以參數(shù)來(lái)指定。
internal sealed class DedicatedThreadTaskScheduler : TaskScheduler { private readonly BlockingCollection<Task> _tasks = new(); private readonly Thread[] _threads; protected override IEnumerable<Task>? GetScheduledTasks() => _tasks; protected override void QueueTask(Task task) => _tasks.Add(task); protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => false; public DedicatedThreadTaskScheduler(int threadCount) { _threads = new Thread[threadCount]; for (int index = 0; index < threadCount; index++) { _threads[index] = new Thread(_ => { while (true) { TryExecuteTask(_tasks.Take()); } }); } Array.ForEach(_threads, it => it.Start()); } }
我們演示實(shí)例中Run/Do方法再次還原成如下所示的純異步模式的RunAsync/DoAsync,并在調(diào)用StartNew方法的時(shí)候創(chuàng)建一個(gè)DedicatedThreadTaskScheduler對(duì)象作為最后一個(gè)參數(shù)。
Task.Factory.StartNew(RunAsync, CancellationToken.None, TaskCreationOptions.LongRunning, new DedicatedThreadTaskScheduler(1)); Console.Read(); async Task RunAsync() { while (true) { await DoAsync(); } } async Task DoAsync() { await Task.Delay(2000); var isThreadPoolThread = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"[{DateTimeOffset.Now}]Is thread pool thread: {isThreadPoolThread}"); }
由于創(chuàng)建的Task將會(huì)使用指定的DedicatedThreadTaskScheduler 對(duì)象來(lái)調(diào)度,DoAsync方法自然就不會(huì)在線程池線程中執(zhí)行了。
七、獨(dú)立線程池
.NET提供的線程池是一個(gè)全局共享的線程池,而我們定義的DedicatedThreadTaskScheduler相當(dāng)于創(chuàng)建了一個(gè)獨(dú)立的線程池,對(duì)象池的效果可以通過(guò)如下這個(gè)簡(jiǎn)單的程序展現(xiàn)出來(lái)。
Task.Factory.StartNew(()=> Task.WhenAll( Enumerable.Range(1,6).Select(it=>DoAsync(it))), CancellationToken.None, TaskCreationOptions.None, new DedicatedThreadTaskScheduler(2)); async Task DoAsync(int index) { await Task.Yield(); Console.WriteLine($"[{DateTimeOffset.Now.ToString("hh:MM:ss")}]Task {index} is executed in thread {Environment.CurrentManagedThreadId}"); var endTime = DateTime.UtcNow.AddSeconds(4); SpinWait.SpinUntil(() => DateTime.UtcNow > endTime); await Task.Delay(1000); } Console.ReadLine();
如上面的代碼片段所示,異步方法DoAsync利用自旋等待模擬了一段耗時(shí)4秒的操作,通過(guò)調(diào)用Task.Delay方法模擬了一段耗時(shí)1秒的IO操作。我們?cè)谄渲休敵隽巳蝿?wù)開(kāi)始執(zhí)行的時(shí)間和當(dāng)前線程ID。在調(diào)用的StartNew方法中,我們調(diào)用這個(gè)DoAsync方法創(chuàng)建了6個(gè)Task,這些Task交給創(chuàng)建的DedicatedThreadTaskScheduler進(jìn)行調(diào)度。我們?yōu)檫@個(gè)DedicatedThreadTaskScheduler指定的線程數(shù)量為2。從如下所示的輸出結(jié)果可以看出,6個(gè)操作確實(shí)在兩個(gè)線程中執(zhí)行的。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-462940.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-462940.html
到了這里,關(guān)于如何讓Task在非線程池線程中執(zhí)行?的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!