我沒(méi)能實(shí)現(xiàn)始終在一個(gè)線程上運(yùn)行 task

前文我們總結(jié)了在使用常駐任務(wù)實(shí)現(xiàn)常駐線程時(shí),應(yīng)該注意的事項(xiàng)。但是我們最終沒(méi)有提到如何在處理對(duì)于帶有異步代碼的辦法。本篇將接受筆者對(duì)于該內(nèi)容的總結(jié)。
如何識(shí)別當(dāng)前代碼跑在什么線程上
一切開(kāi)始之前,我們先來(lái)使用一種簡(jiǎn)單的方式來(lái)識(shí)別當(dāng)前代碼運(yùn)行在哪種線程上。
最簡(jiǎn)單的方式就是打印當(dāng)前線程名稱和線程ID來(lái)識(shí)別。
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

通過(guò)這段代碼,我們可以非常容易的識(shí)別三種不同情況下的線程信息。
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

分別為:
自定義線程 Custom thread
線程池線程 .NET ThreadPool Worker
由 Task.Factory.StartNew 創(chuàng)建的新線程 .NET Long Running Task
因此,結(jié)合我們之前曇花線程的例子,我們也可以非常簡(jiǎn)單的看出線程的切換情況:
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

我們希望在同一個(gè)線程上運(yùn)行 Task 代碼
之前我們已經(jīng)知道了,手動(dòng)創(chuàng)建線程并控制線程的運(yùn)行,可以確保自己的代碼不會(huì)于線程池線程產(chǎn)生競(jìng)爭(zhēng),從而使得我們的常駐任務(wù)能夠穩(wěn)定的觸發(fā)。
當(dāng)時(shí)用于演示的錯(cuò)誤示例是這樣的:
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

這個(gè)示例可以明顯的看出,中間的部分代碼是運(yùn)行在線程池的。這種做法會(huì)在線程池資源緊張的時(shí)候,導(dǎo)致我們的常駐任務(wù)無(wú)法觸發(fā)。
因此,我們需要一種方式來(lái)確保我們的代碼在同一個(gè)線程上運(yùn)行。
那么接下來(lái)我們分析一些想法和效果。
加配!加配!加配!
我們已經(jīng)知道了,實(shí)際上,常駐任務(wù)不能穩(wěn)定觸發(fā)是因?yàn)?Task 會(huì)在線程池中運(yùn)行。那么增加線程池的容量自然就是最直接解決高峰的做法。 因此,如果條件允許的話,直接增加 CPU 核心數(shù)實(shí)際上是最為有效和簡(jiǎn)單的方式。
不過(guò)這種做法并不適用于一些類庫(kù)的編寫(xiě)者。比如,你在編寫(xiě)日志類庫(kù),那么其實(shí)無(wú)法欲知用戶所處的環(huán)境。并且正如大家所見(jiàn),市面上幾乎沒(méi)有日志類庫(kù)中由說(shuō)明讓用戶只能在一定的 CPU 核心數(shù)下使用。
因此,如果您的常駐任務(wù)是在類庫(kù)中,那么我們需要一種更為通用的方式來(lái)解決這個(gè)問(wèn)題。
考慮使用同步重載
在 Task 出現(xiàn)之后,很多時(shí)候我們都會(huì)考慮使用異步重載的方法。這顯然不是錯(cuò)誤的做法,因?yàn)檫@可以使得我們的代碼更加高效,提升系統(tǒng)的吞吐量。但是,如果你想要讓 Thread 穩(wěn)定的在同一個(gè)線程上運(yùn)行,那么你需要考慮使用同步重載的方法。通過(guò)同步重載方法,我們的代碼將不會(huì)出現(xiàn)線程切換到線程池的情況。自然也就實(shí)現(xiàn)了我們的目的。
總是使用 TaskCreationOptions.LongRunning
這個(gè)辦法其實(shí)很不實(shí)際。因?yàn)槿魏我粚記](méi)有指定,都會(huì)將任務(wù)切換到線程池中。
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

所以說(shuō),這個(gè)辦法可以用。但其實(shí)很怪。
自定義 Scheduler
這是一種可行,但是非常困難的做法。雖然說(shuō)自定義個(gè)簡(jiǎn)單的 Scheduler 也不是很難,只需要實(shí)現(xiàn)幾個(gè)簡(jiǎn)單的方法。但要按照我們的需求來(lái)實(shí)現(xiàn)這個(gè) Scheduler 并不簡(jiǎn)單。
比如我們嘗試實(shí)現(xiàn)一個(gè)這樣的 Scheduler:
注意:這個(gè) Scheduler 并不能正常工作。
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

上面的代碼中,我們期待通過(guò)一個(gè)單一的線程來(lái)執(zhí)行所有的任務(wù)。但實(shí)際上它反而是一個(gè)非常簡(jiǎn)單的死鎖演示裝置。
我們?cè)O(shè)想運(yùn)行下面這段代碼:
Bilibili 代碼塊無(wú)法正常渲染,因此無(wú)法正常顯示。請(qǐng)關(guān)注微信公眾號(hào)“newbe技術(shù)專欄”,搜索對(duì)應(yīng)文章代碼內(nèi)容。

這段代碼中,我們期待,在一個(gè) Task 中運(yùn)行另外一個(gè) Task。但實(shí)際上,這段代碼會(huì)死鎖。
因?yàn)?,我們?MyScheduler 中,我們?cè)谝粋€(gè)死循環(huán)中,不斷的從隊(duì)列中取出任務(wù)并執(zhí)行。但是,我們的任務(wù)中,又會(huì)調(diào)用 Wait 方法。
我們不妨設(shè)想這個(gè)線程就是我們自己。
首先,老板交代給你一件任務(wù),你把它放到隊(duì)列中。
然后你開(kāi)始執(zhí)行這件任務(wù),執(zhí)行到一半發(fā)現(xiàn),你需要等待第二件任務(wù)的執(zhí)行結(jié)果。因此你在這里等著。
但是第二件任務(wù)這個(gè)時(shí)候也塞到了你的隊(duì)列中。
這下好了,你手頭的任務(wù)在等待你隊(duì)列里面的任務(wù)完成。而你隊(duì)列的任務(wù)只有你才能完成。
完美卡死。
因此,其實(shí)實(shí)際上我們需要在 Wait 的時(shí)候通知當(dāng)前線程,此時(shí)線程被 Block 了,然后轉(zhuǎn)而從隊(duì)列中取出任務(wù)執(zhí)行。在 Task 于 ThreadPool 的配合中,是存在這樣的機(jī)制的。但是,我們自己實(shí)現(xiàn)的 MyScheduler 并不能與 Task 產(chǎn)生這種配合。因此需要考慮自定義一個(gè) Task。跟進(jìn)一步說(shuō),我們需要自定義 AsyncMethodBuilder 來(lái)實(shí)現(xiàn)全套的自定義。
顯然者是一項(xiàng)相對(duì)高級(jí)內(nèi)容,期待了解的讀者,可以通過(guò) UniTask1 項(xiàng)目來(lái)了解如何實(shí)現(xiàn)這樣的全套自定義。
總結(jié)
如果你期望在常駐線程能夠穩(wěn)定的運(yùn)行你的任務(wù)。那么:
加配,以避免線程池不夠用
考慮在這部分代碼中使用同步代碼
可以學(xué)習(xí)自定義 Task 系統(tǒng)
參考
.NET Task 揭秘(2):Task 的回調(diào)執(zhí)行與 await2
Task3
TaskCreationOptions4
這樣在 C# 使用 LongRunningTask 是錯(cuò)的5
async 與 Thread 的錯(cuò)誤結(jié)合6
實(shí)現(xiàn)常駐任務(wù)除了避免曇花線程,還需要避免重返線程池7
感謝您的閱讀,如果您覺(jué)得本文有用,快長(zhǎng)按右下角大拇指??為本文點(diǎn)贊~
歡迎關(guān)注作者的微信公眾號(hào)“newbe技術(shù)專欄”,獲取更多技術(shù)內(nèi)容。

本文作者: newbe36524
本文鏈接: https://www.newbe.pro/Others/0x029-I-can-not-manage-to-always-run-task-on-one-thread/
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協(xié)議。轉(zhuǎn)載請(qǐng)注明出處!
https://github.com/Cysharp/UniTask?
https://www.cnblogs.com/eventhorizon/p/15912383.html?
https://threads.whuanle.cn/3.task/?
https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0&WT.mc_id=DX-MVP-5003606?
https://www.newbe.pro/Others/0x026-This-is-the-wrong-way-to-use-LongRunnigTask-in-csharp/?
https://www.newbe.pro/Others/0x027-error-when-using-async-with-thread/?
https://www.newbe.pro/Others/0x028-avoid-return-to-threadpool-in-longrunning-task?