<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          C#同步方法中如何調(diào)用異步方法?

          共 5567字,需瀏覽 12分鐘

           ·

          2022-10-15 04:42







          前言

          我在寫(xiě)代碼的時(shí)候(.net core)有時(shí)候會(huì)碰到void方法里,調(diào)用async方法并且Wait,而且我還看到別人這么寫(xiě)了。而且我這么寫(xiě)的時(shí)候,編譯器沒(méi)有提示任何警告。但是看了dudu的文章:一碼阻塞,萬(wàn)碼等待:ASP.NET Core 同步方法調(diào)用異步方法“死鎖”的真相 了解了,這樣寫(xiě)是有問(wèn)題的。但是為什么會(huì)有問(wèn)題呢?我又閱讀了dudu文章里提到的一篇博文:.NET Threadpool starvation, and how queuing makes it worse 加上自己親手實(shí)驗(yàn),寫(xiě)下自己的理解,算是對(duì)dudu博文的一個(gè)補(bǔ)充和豐富吧。

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

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

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

          這兩種場(chǎng)景都沒(méi)有編譯錯(cuò)誤。
          首先我們來(lái)看一下,在 void里調(diào)用 async 方法,并且要等待async的結(jié)果出來(lái)之后,才能進(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()。咱們來(lái)運(yùn)行一遍。

          沒(méi)有任何問(wèn)題??雌饋?lái),這樣寫(xiě)完全沒(méi)有問(wèn)題啊,不報(bào)錯(cuò),運(yùn)行也是正常的。
          接下來(lái),我們修改一下代碼,讓代碼更加接近生產(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。
          接下來(lái)我們?cè)賵?zhí)行下,看看結(jié)果:

          在第一秒里只執(zhí)行了兩次Task,就卡住了。我們?cè)倏聪逻M(jìn)程信息:

          沒(méi)有CPU消耗,但是線程數(shù)一直增加,直到突破一臺(tái)電腦的最大線程數(shù),導(dǎo)致服務(wù)器宕機(jī)。
          這明顯出現(xiàn)問(wè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,從第三秒開(kāi)始,就穩(wěn)定執(zhí)行了4-5次Task,這里的時(shí)間統(tǒng)計(jì)不是很精確,但是可以肯定從某個(gè)時(shí)間開(kāi)始,程序達(dá)到了預(yù)期效果,TreadPool中的線程每秒中都能穩(wěn)定的完成任務(wù)。而且我們還能觀察到,在最開(kāi)始,程序是反應(yīng)很慢的,那個(gè)時(shí)候線程不夠用,同時(shí)應(yīng)該在申請(qǐng)新的線程,直到后來(lái)線程足夠處理這樣的情況了。咱們?cè)倏纯催@個(gè)時(shí)候的進(jìn)程信息:

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

          探究原因

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

          咱們回到一開(kāi)始的代碼里,我們加上一個(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),是沒(méi)問(wèn)題的。說(shuō)明一開(kāi)始設(shè)置多的線程是有用的,經(jīng)過(guò)實(shí)驗(yàn)發(fā)現(xiàn),只要初始線程小于10個(gè),都會(huì)出現(xiàn)死鎖。而.net core的默認(rèn)初始線程是肯定小于10個(gè)的。

          那么當(dāng)初始線程小于10個(gè)的時(shí)候,發(fā)生什么了?發(fā)生了大家都聽(tīng)說(shuō)過(guò)的名詞,線程饑餓。就是線程不夠用了,這個(gè)時(shí)候ThreadPool生產(chǎn)新的線程滿足需求。

          然后我們?cè)訇P(guān)注下,同步方法里調(diào)用異步方法并且.Wait()的情況下會(huì)發(fā)生什么。

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

          首先有一個(gè)線程A ,開(kāi)始執(zhí)行Producer , 它執(zhí)行到了Process 的時(shí)候,新產(chǎn)生了一個(gè)的線程 B 去執(zhí)行這個(gè)Task。這個(gè)時(shí)候 A 會(huì)掛起,一直等 B 結(jié)束,B被釋放,然后A繼續(xù)執(zhí)行剩下的過(guò)程。這樣執(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 并沒(méi)有等B,而是被ThreadPool拿來(lái)做別的事情,等B結(jié)束之后,ThreadPool 再拿一個(gè)線程出來(lái)執(zhí)行剩下的部分。所以這個(gè)過(guò)程是沒(méi)有線程阻塞的。

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


          于是ThreadPool 又要去產(chǎn)生4個(gè)新的線程去滿足4個(gè)被阻塞的Task,花了2秒時(shí)間,終于生產(chǎn)完了。但是糟糕又來(lái)了10個(gè)Task,需要20個(gè)線程,而之前釋放的線程已經(jīng)不足以讓任何一個(gè)Task去執(zhí)行Process了,因?yàn)檫@些不足的線程都被分配到了Producer上,沒(méi)有線程再可以去執(zhí)行Process了(經(jīng)過(guò)上面的分析一個(gè)Task需要2個(gè)線程A,B,并且A阻塞,直到B執(zhí)行Process完成)。
          所以隨著時(shí)間的流逝,要執(zhí)行的Task越來(lái)越多卻沒(méi)有一個(gè)能執(zhí)行結(jié)束,而線程也在不斷產(chǎn)生,就產(chǎn)生了我們上面所說(shuō)的情況。

          我們?cè)撛趺崔k?

          經(jīng)過(guò)上面的分析我們知道,在線程饑餓的情況下,使用同步方法調(diào)用異步方法并且wait結(jié)果,是會(huì)出問(wèn)題的,那么我們應(yīng)該怎么辦呢?
          首先當(dāng)然是應(yīng)該避免這種有風(fēng)險(xiǎn)的做法。

          其次,還有一種方法。經(jīng)過(guò)實(shí)驗(yàn),我發(fā)現(xiàn),使用專有線程

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

          就是TaskCreationOptions.LongRunning 選項(xiàng),就是開(kāi)辟一個(gè)專用線程,而不是在ThreadPool中拿線程,這樣是不會(huì)發(fā)生死鎖的。
          因?yàn)門(mén)hreadPool 不管理專用線程,每一個(gè)Task進(jìn)來(lái),都會(huì)有專門(mén)的線程執(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)建出來(lái)后,先看看自己的工作隊(duì)列有沒(méi)有被分配task,如果沒(méi)有的話,就去global queue找task,如果還沒(méi)有的話,就去別的線程的工作隊(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

          瀏覽 21
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码三级电影片 | 狂野欧美做受XXXX高潮 | 欧美丰满大爆乳波霸奶 | 亚洲乱伦电影 | 国产麻豆成人A |