<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)用異步方法?

          共 11549字,需瀏覽 24分鐘

           ·

          2020-10-13 03:04

          原文鏈接:

          https://www.cnblogs.com/dacc123/p/12796578.html


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

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

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

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

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

          這兩種場景都沒有編譯錯(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<boolProcess()
                  {
                      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<boolProcess()
                  {
                      await Task.Run(() =>
                      {
                          Thread.Sleep(1000);
                      });

                      Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString());
                      return true;
                  }
              }
          }

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

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

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

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

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

          我們再M(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)該在申請新的線程,直到后來線程足夠處理這樣的情況了。咱們再看看這個(gè)時(shí)候的進(jìn)程信息:

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

          探究原因

          我們再深層次討論下為什么同步方法里調(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(1010);

                          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)新的線程滿足需求。然后我們再關(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)生了我們上面所說的情況。## 我們該怎么辦?經(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é)語

          關(guān)于ThreadPool 中的線程調(diào)用算法,其實(shí)很簡單,每個(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)先級很高,所有新產(chǎn)生的線程都去執(zhí)行g(shù)lobal Task,而global task又需要一個(gè)線程去執(zhí)行l(wèi)ocal task,所以產(chǎn)生了死鎖。

          回復(fù) 【關(guān)閉】學(xué)關(guān)閉微信朋友圈廣告
          回復(fù) 【實(shí)戰(zhàn)】獲取20套實(shí)戰(zhàn)源碼
          回復(fù) 【被刪】學(xué)查看你哪個(gè)好友刪除了你巧
          回復(fù) 【訪客】學(xué)微信查看朋友圈訪客記錄
          回復(fù) 【小程序】學(xué)獲取15套【入門+實(shí)戰(zhàn)+賺錢】小程序源碼
          回復(fù) 【python】學(xué)微獲取全套0基礎(chǔ)Python知識(shí)手冊
          回復(fù) 【2019】獲取2019 .NET 開發(fā)者峰會(huì)資料PPT
          回復(fù) 【加群】加入dotnet微信交流群

          一招搞定github下載速度到2MB/s


          良心推薦:.NET Core快速開發(fā)利器WTM!


          瀏覽 50
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  wwww国产精品 | 青娱乐在线国产 | 久9在线视频 | 国产各种高潮视频在线播放 | 成人肏屄网站 |