日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > C# >内容正文

C#

channelread0会被调用两次_值得一看:C#同步方法中如何调用异步方法?

發(fā)布時(shí)間:2025/3/20 C# 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 channelread0会被调用两次_值得一看:C#同步方法中如何调用异步方法? 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

我在寫代碼的時(shí)候(.net core)有時(shí)候會(huì)碰到void方法里,調(diào)用async方法并且Wait,而且我還看到別人這么寫了。而且我這么寫的時(shí)候,編譯器沒有提示任何警告。但是看了dudu的文章:一碼阻塞,萬(wàn)碼等待:ASP.NET Core 同步方法調(diào)用異步方法“死鎖”的真相 了解了,這樣寫是有問題的。

但是為什么會(huì)有問題呢?

我又閱讀了dudu文章里提到的一篇博文:.NET Threadpool starvation, and how queuing makes it worse 加上自己親手實(shí)驗(yàn),寫下自己的理解,算是對(duì)dudu博文的一個(gè)補(bǔ)充和豐富吧。

同步方法里調(diào)用異步方法

同步方法里調(diào)用異步方法,一種是wait() 一種是不wait()

void fun() { funAsync.Wait();funAsync(); }

這兩種場(chǎng)景都沒有編譯錯(cuò)誤。 首先我們來看一下,在 void里調(diào)用 async 方法,并且要等待async的結(jié)果出來之后,才能進(jìn)行后續(xù)的操作。

using System; using System.Threading; using System.Threading.Tasks;namespace ConsoleTool2 {class Program{static void Main(string[] args){Producer();}static void Producer(){var result = Process().Result;//或者//Process().Wait();}static async Task<bool> Process(){await Task.Run(() =>{Thread.Sleep(1000);});Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());return true;}} }

咱們看這個(gè)Producer,這是一個(gè)void方法,里面調(diào)用了異步方法Process(),其中Process()是一個(gè)執(zhí)行1秒的異步方法,調(diào)用的方式是Process().Result 或者Process().Wait(),咱們來運(yùn)行一遍。

沒有任何問題。看起來,這樣寫完全沒有問題啊,不報(bào)錯(cuò),運(yùn)行也是正常的。 接下來,我們修改一下代碼,讓代碼更加接近生產(chǎn)環(huán)境的狀態(tài)。

using System; using System.Threading; using System.Threading.Tasks;namespace ConsoleTool2 {class Program{static void Main(string[] args){while (true){Task.Run(Producer);Thread.Sleep(200);}}static void Producer(){var result = Process().Result;}static async Task<bool> Process(){await Task.Run(() =>{Thread.Sleep(1000);});Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());return true;}} }

我們?cè)贛ain函數(shù)里加了for循環(huán),并且1秒鐘執(zhí)行5次Producer(),使用Task.Run(),1秒鐘有5個(gè)Task產(chǎn)生。相當(dāng)于生產(chǎn)環(huán)境的qps=5。 接下來我們?cè)賵?zhí)行下,看看結(jié)果:

沒有CPU消耗,但是線程數(shù)一直增加,直到突破一臺(tái)電腦的最大線程數(shù),導(dǎo)致服務(wù)器宕機(jī)。 這明顯出現(xiàn)問題了,線程肯定發(fā)生了死鎖,而且還在不斷產(chǎn)生新的線程。

至于為什么只執(zhí)行了兩次Task,我們可以猜測(cè)是因?yàn)槌绦蛑谐跏嫉腡readPool 中只有兩個(gè)線程,所以執(zhí)行了兩次Task,然后就發(fā)生了死鎖。

現(xiàn)在我們定義一個(gè)Produce2() 這是一個(gè)正常的方法,異步函數(shù)調(diào)用異步函數(shù)。

static async Task Producer2(){await Process();}

我們?cè)費(fèi)ain函數(shù)的循環(huán)里,執(zhí)行Producer2() ,執(zhí)行信息如下:

仔細(xì)觀察這個(gè)圖,我們發(fā)現(xiàn)第一秒執(zhí)行了一個(gè)Task,第二秒執(zhí)行了三個(gè)Task,從第三秒開始,就穩(wěn)定執(zhí)行了4-5次Task,這里的時(shí)間統(tǒng)計(jì)不是很精確,但是可以肯定從某個(gè)時(shí)間開始,程序達(dá)到了預(yù)期效果,TreadPool中的線程每秒中都能穩(wěn)定的完成任務(wù)。而且我們還能觀察到,在最開始,程序是反應(yīng)很慢的,那個(gè)時(shí)候線程不夠用,同時(shí)應(yīng)該在申請(qǐng)新的線程,直到后來線程足夠處理這樣的情況了。咱們?cè)倏纯催@個(gè)時(shí)候的進(jìn)程信息:

線程數(shù)一直穩(wěn)定在25個(gè),也就是說25個(gè)線程就能滿足這個(gè)程序的運(yùn)行了。 到此我們可以證明,在同步方法里調(diào)用異步方法確實(shí)是不安全的,尤其在并發(fā)量很高的情況下。

探究原因

我們?cè)偕顚哟斡懻撓聻槭裁赐椒椒ɡ镎{(diào)用異步方法會(huì)卡死,而異步方法調(diào)用異步方法則很安全呢?

咱們回到一開始的代碼里,我們加上一個(gè)初始化線程數(shù)量的代碼,看看這樣是否還是會(huì)出現(xiàn)卡死的狀況。 由于前面的分析我們知道,這個(gè)程序在一秒中并行執(zhí)行5個(gè)Task,每個(gè)Task里面也就是Producer 都會(huì)執(zhí)行一個(gè)Processer 異步方法,所以粗略估計(jì)需要10個(gè)線程。于是我們就初始化線程數(shù)為10個(gè)。

using System;using System.Threading;using System.Threading.Tasks;namespace ConsoleTool2{class Program{static void Main(string[] args) {ThreadPool.SetMinThreads(10, 10);while (true){Task.Run(Producer2);Thread.Sleep(200);}}static void Producer() {var result = Process().Result;}static async Task Producer2() {await Process();}static async Task<bool> Process() {await Task.Run(() =>{Thread.Sleep(1000);});Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());return true;}}}

運(yùn)行一下發(fā)現(xiàn),是沒問題的。說明一開始設(shè)置多的線程是有用的,經(jīng)過實(shí)驗(yàn)發(fā)現(xiàn),只要初始線程小于10個(gè),都會(huì)出現(xiàn)死鎖。而.net core的默認(rèn)初始線程是肯定小于10個(gè)的。 那么當(dāng)初始線程小于10個(gè)的時(shí)候,發(fā)生什么了?發(fā)生了大家都聽說過的名詞,線程饑餓。就是線程不夠用了,這個(gè)時(shí)候ThreadPool生產(chǎn)新的線程滿足需求。 然后我們?cè)訇P(guān)注下,同步方法里調(diào)用異步方法并且.Wait()的情況下會(huì)發(fā)生什么。

void Producer(){Process().Wait()}

首先有一個(gè)線程A ,開始執(zhí)行Producer , 它執(zhí)行到了Process 的時(shí)候,新產(chǎn)生了一個(gè)的線程 B 去執(zhí)行這個(gè)Task。這個(gè)時(shí)候 A 會(huì)掛起,一直等 B 結(jié)束,B被釋放,然后A繼續(xù)執(zhí)行剩下的過程。這樣執(zhí)行一次Producer 會(huì)用到兩個(gè)線程,并且A 一直掛起,一直不工作,一直在等B。這個(gè)時(shí)候線程A 就會(huì)阻塞。

Task Producer(){await Process();}

這個(gè)和上面的區(qū)別就是,同時(shí)線程A,它執(zhí)行到Producer的時(shí)候,產(chǎn)生了一個(gè)新的線程B執(zhí)行 Process。

但是 A 并沒有等B,而是被ThreadPool拿來做別的事情,等B結(jié)束之后,ThreadPool 再拿一個(gè)線程出來執(zhí)行剩下的部分。所以這個(gè)過程是沒有線程阻塞的。

再結(jié)合線程饑餓的情況,也就是ThreadPool 中發(fā)生了線程阻塞+線程饑餓,會(huì)發(fā)生什么呢? 假設(shè)一開始只有8個(gè)線程,第一秒中會(huì)并行執(zhí)行5個(gè)Task Producer, 5個(gè)線程被拿來執(zhí)行這5個(gè)Task,然后這個(gè)5個(gè)線程(A)都在阻塞,并且ThreadPool 被要求再拿5個(gè)線程(B)去執(zhí)行Process,但是線程池只剩下3個(gè)線程,所以ThreadPool 需要再產(chǎn)生2個(gè)線程來滿足需求。但是ThreadPool 1秒鐘最多生產(chǎn)2個(gè)線程,等這2個(gè)線程被生產(chǎn)出來以后,又過去了1秒,這個(gè)時(shí)候無情又進(jìn)來5個(gè)Task,又需要10個(gè)線程了。

別忘了執(zhí)行第一波Task的一些線程應(yīng)該釋放了,釋放多少個(gè)呢?應(yīng)該是3個(gè)Task占有的線程,因?yàn)橛?個(gè)在等TreadPool生產(chǎn)新線程嘛。所以釋放了6個(gè)線程,5個(gè)Task,6個(gè)線程,計(jì)算一下,就可以知道,只有一個(gè)Task可以被完全執(zhí)行,其他4個(gè)都因?yàn)闆]有新的線程執(zhí)行Process而阻塞。

于是ThreadPool 又要去產(chǎn)生4個(gè)新的線程去滿足4個(gè)被阻塞的Task,花了2秒時(shí)間,終于生產(chǎn)完了。但是糟糕又來了10個(gè)Task,需要20個(gè)線程,而之前釋放的線程已經(jīng)不足以讓任何一個(gè)Task去執(zhí)行Process了,因?yàn)檫@些不足的線程都被分配到了Producer上,沒有線程再可以去執(zhí)行Process了(經(jīng)過上面的分析一個(gè)Task需要2個(gè)線程A,B,并且A阻塞,直到B執(zhí)行Process完成)。

所以隨著時(shí)間的流逝,要執(zhí)行的Task越來越多卻沒有一個(gè)能執(zhí)行結(jié)束,而線程也在不斷產(chǎn)生,就產(chǎn)生了我們上面所說的情況。 ## 我們?cè)撛趺崔k? 經(jīng)過上面的分析我們知道,在線程饑餓的情況下,使用同步方法調(diào)用異步方法并且wait結(jié)果,是會(huì)出問題的,那么我們應(yīng)該怎么辦呢? 首先當(dāng)然是應(yīng)該避免這種有風(fēng)險(xiǎn)的做法。 其次,還有一種方法。經(jīng)過實(shí)驗(yàn),我發(fā)現(xiàn),使用專有線程

Task.Run(Producer);改成Task.Factory.StartNew(Producer,TaskCreationOptions.LongRunning);

就是TaskCreationOptions.LongRunning 選項(xiàng),就是開辟一個(gè)專用線程,而不是在ThreadPool中拿線程,這樣是不會(huì)發(fā)生死鎖的。

因?yàn)門hreadPool 不管理專用線程,每一個(gè)Task進(jìn)來,都會(huì)有專門的線程執(zhí)行,而Process 則是由ThreadPool 中的線程執(zhí)行,這樣TheadPool中的線程其實(shí)是不存在阻塞的,因此也不存在死鎖。

結(jié)語(yǔ)

關(guān)于ThreadPool 中的線程調(diào)用算法,其實(shí)很簡(jiǎn)單,每個(gè)線程都有一個(gè)自己的工作隊(duì)列l(wèi)ocal queue,此外線程池中還有一個(gè)global queue全局工作隊(duì)列,首先一個(gè)線程被創(chuàng)建出來后,先看看自己的工作隊(duì)列有沒有被分配task,如果沒有的話,就去global queue找task,如果還沒有的話,就去別的線程的工作隊(duì)列找Task。

第二種情況:在同步方法里調(diào)用異步方法,不wait()

如果這個(gè)異步方法進(jìn)入的是global Task 則在線程饑餓的情況下,也會(huì)發(fā)生死鎖的情況。至于為什么,可以看那篇博文里的解釋,因?yàn)間lobal Task的優(yōu)先級(jí)很高,所有新產(chǎn)生的線程都去執(zhí)行g(shù)lobal Task,而global task又需要一個(gè)線程去執(zhí)行l(wèi)ocal task,所以產(chǎn)生了死鎖。

原文鏈接:https://www.cnblogs.com/dacc123/p/12796578.html

總結(jié)

以上是生活随笔為你收集整理的channelread0会被调用两次_值得一看:C#同步方法中如何调用异步方法?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。