Task、await、async的使用
異步與同步》
異步:不阻塞線程
同步:阻塞線程
異步方法、同步方法》
即某一方法在執(zhí)行時如果后續(xù)的代碼不能執(zhí)行只有等待該方法執(zhí)行完成后才能繼續(xù)執(zhí)行(造成UI卡死現(xiàn)象)的方法稱之為同步方法,反之如果該方法在執(zhí)行階段后續(xù)代碼依舊可以執(zhí)行,這個方法稱之為異步方法。
多線程編寫》
Thread》
之前使用最多的一種。
看一個例子【輸出1到10每秒輸出一個數(shù)字】
? ? ? ?static void Main(string[] args)
? ? ? ?{
??
? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Thread th = new Thread(() =>
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int index = 1; index <11; index++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?Console.WriteLine(index);
? ? ? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?th.Start();
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
上述是最基本的使用,關(guān)于Thread不多做介紹。
ThreadPool》
ThreadPool(線程池)是一種并發(fā)編程的概念,它是一組可重復(fù)使用的線程,用于執(zhí)行多個任務(wù)。線程池管理著一個線程隊列,其中包含多個線程,這些線程可以執(zhí)行任務(wù)。
使用線程池的好處是可以避免頻繁地創(chuàng)建和銷毀線程,從而提高程序的性能和效率。線程池可以控制并發(fā)線程的數(shù)量,避免系統(tǒng)資源被過度占用。當(dāng)有任務(wù)需要執(zhí)行時,線程池中的線程會被分配給任務(wù),并在任務(wù)完成后返回線程池,等待下一個任務(wù)的到來。
線程池通常由以下幾個組件組成:
任務(wù)隊列:用于存儲待執(zhí)行的任務(wù)。
線程池管理器:用于創(chuàng)建、銷毀和管理線程池中的線程。
工作線程:線程池中的線程,用于執(zhí)行任務(wù)。它們從任務(wù)隊列中獲取任務(wù)并執(zhí)行,執(zhí)行完任務(wù)后返回線程池等待下一個任務(wù)。
一個案例:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?ThreadPool.QueueUserWorkItem(new WaitCallback((t) => {
? ? ? ? ? ? ? ? ? ?Console.WriteLine($"這是第{t}個任務(wù)~");
? ? ? ? ? ? ? ?}), i);
? ? ? ? ? ?}
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
ThreadPool幾乎沒用過(慚愧?。?/p>
Task》
重點介紹一下Task
Task與Thread相比有哪些優(yōu)缺點?
Task(任務(wù))和Thread(線程)是并發(fā)編程中常用的兩個概念,它們有各自的優(yōu)缺點。
優(yōu)點:
靈活性:Task比Thread更加靈活。Task通常是以異步的方式執(zhí)行,可以在需要時啟動、暫停、取消或等待任務(wù)的完成。而Thread是同步執(zhí)行的,一旦啟動就會一直執(zhí)行直到結(jié)束。
資源消耗:Task比Thread消耗的資源更少。Task利用線程池來執(zhí)行任務(wù),可以重用線程,避免頻繁創(chuàng)建和銷毀線程的開銷,從而減少系統(tǒng)資源的消耗。
異常處理:Task可以更好地處理異常。Task可以通過異常處理機制捕獲和處理任務(wù)中的異常,而Thread需要開發(fā)者自行處理異常。
缺點:
復(fù)雜性:相比于Thread,Task的使用可能更加復(fù)雜
Task的創(chuàng)建》還是【輸出1到10每秒輸出一個數(shù)字】為例
方法1
如下創(chuàng)建方式
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Task tk = new Task(()=>
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?tk.Start();
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
方法二:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Task tk = Task.Factory.StartNew(() =>
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
可以看到我并沒有使用tk.Start();來啟動,因為這種創(chuàng)建方式當(dāng)創(chuàng)建好后就表示啟動(窗機即啟動)。
Task tk = new Task()
:這種方式只是創(chuàng)建了一個Task
對象,但并沒有立即啟動它。需要調(diào)用tk.Start()
方法來手動啟動任務(wù)。這種方式適用于需要手動控制任務(wù)的啟動時機,或者需要在任務(wù)啟動前進行一些其他操作的情況。Task.Factory.StartNew()
:這種方式創(chuàng)建并立即啟動一個Task
對象。它使用Task
類的工廠方法來創(chuàng)建任務(wù),并自動啟動任務(wù)。這種方式更為簡潔,適用于直接啟動任務(wù)且不需要手動控制啟動時機的情況。
方法三:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Task tk = Task.Run(() =>
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
Task tk = Task.Run():這種方式是.NET Framework 4.5及更高版本引入的簡化創(chuàng)建和啟動任務(wù)的方法。它會創(chuàng)建并立即啟動一個 Task 對象,類似于 Task.Factory.StartNew(),但是更為簡潔。它會自動使用默認(rèn)的 TaskScheduler 來調(diào)度任務(wù),并且返回一個已啟動的 Task 對象。
關(guān)于await、async》
這兩玩意是干啥的?
解釋這個問題先看一下之前寫的同步方法、與異步方法。
還是以【輸出1到10每秒輸出一個數(shù)字】為例說明。
同步方法最基本的寫法來完成這個需求:
static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ?}
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}

執(zhí)行結(jié)果沒毛病,是每秒輸出一個。但是我代碼后面有一句[Console.ReadLine();]讀取用戶輸入的操作,在代碼輸出1-10期間我是不能輸入的因為這是同步方法,我的UI是卡死的(控制臺程序還不太能看出來卡死在大多數(shù)窗體應(yīng)用中非常明顯,這樣用戶體驗會非常差),所以來看看異步方法。
確保畫面不卡死的情況下完成這個需求:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Task tk = Task.Factory.StartNew(() =>
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?Thread.Sleep(1000);
? ? ? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}

當(dāng)數(shù)字在輸出階段我還是可以手動輸入內(nèi)容的,這樣就不會造成UI卡死的現(xiàn)象。
但是這并不是一個異步方法來完成的。
Task.Factory.StartNew
方法用于創(chuàng)建并啟動一個新的 Task
對象。然而,代碼中使用的是 Thread.Sleep
方法而不是 Task.Delay
方法來進行延遲,這意味著任務(wù)是通過線程阻塞來實現(xiàn)延遲的。
由于使用了 Thread.Sleep
方法,這個任務(wù)實際上是同步執(zhí)行的,而不是異步的。
使用異步方法來完成這個需求:
static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ?Task tk = Task.Factory.StartNew(async () =>//async 來修飾異步方法
? ? ? ? ? ?{
? ? ? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ?await Task.Delay(1000);//await 表示等待異步執(zhí)行完成
? ? ? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ? ? ?}
? ? ? ? ? ?});
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
上面是代碼(注釋部分先看一眼,后面解釋),下面是一些截圖

解釋一下異步方法中的awiat、async(重點)》
將上述方法小小的修改一下,先去掉await、async,如何用一個方法來實現(xiàn)一些:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ? ? TaskSleep();
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
? ? ? ?public static void TaskSleep()
? ? ? ?{
? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?Task.Delay(1000);
? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ?}
? ? ? ?}
我調(diào)用寫的TaskSleep來完成輸出,但是效果并不是一秒一個而是瞬間全部輸出,

這是因為Task.Delay()方法是一個異步方法,既然是異步方法,那么這個執(zhí)行就不會阻塞后面的輸出了,我要如何讓后面的輸出等待這個異步方法完成之后再去輸出?
await來了》
為了讓異步方法也可以實現(xiàn)類似于”阻塞“的效果就讓這個await來完成吧

再改一下上述代碼:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ? ? TaskSleep();
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
? ? ? ?public static void TaskSleep()
? ? ? ?{
? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?await Task.Delay(1000);
? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ?}
? ? ? ?}

報錯啊?
這時就要看async了

可以看到await只能用在異步方法中,所以要使用async來修飾方法位異步方法,再改代碼如下:
? ? ? ?static void Main(string[] args)
? ? ? ?{
? ? ? ? ? ?Console.WriteLine("Hello World!");
? ? ? ? ? ? ? TaskSleepAsync();
? ? ? ? ? ?Console.ReadLine();
? ? ? ?}
? ? ? ?public static async Task TaskSleepAsync()
? ? ? ?{
? ? ? ? ? ?for (int i = 1; i <= 10; i++)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?await Task.Delay(1000);
? ? ? ? ? ? ? ?Console.WriteLine(i);
? ? ? ? ? ?}
? ? ? ?}
看一下效果是可以完成需求的標(biāo)準(zhǔn),一秒輸出一個的。

為什么TaskSleepAsync下面有個綠波浪線?

這是因為我的TaskSleepAsync是一個異步方法,但是卻沒有實現(xiàn)等待(await),也就是說并不會等待我的異步方法執(zhí)行完成再執(zhí)行后續(xù)代碼而是直接執(zhí)行后續(xù)代碼,這也就是為什么在輸出進行中時我還能輸入。
修改一些代碼讓這個問題看起來明顯一點:

如果后續(xù)的代碼要等待我異步方法執(zhí)行完后再執(zhí)行,但是我異步方法還沒開始輸出就輸出【異步方法執(zhí)行完成!】是不是很奇怪?
給這個異步方法加上await來試試

這下就沒問題了。
綜上所述,awiat、async就是原來修飾一個方法位異步方法的,await就是來等待這個異步方法完成的,使用await時被修改的方法必須是異步的,而且調(diào)用者本身也是由async所修飾的。
異步方法的返回值》
異步方法也可以有返回值如下:

如果不適應(yīng)await修飾時,返回類型就是一個Task<int>了!

因為沒有使用await修飾所以不會等待異步完成后執(zhí)行后續(xù)代碼,但是異步方法依舊是執(zhí)行的,這點可以從執(zhí)行結(jié)果看出來。

我還可以如下這樣,這時就可以看到等待的效果,而且a執(zhí)行的結(jié)果使用int接收也沒問題。

輸出1到10的內(nèi)容是在TaskSleepAsync
方法中完成的。在TaskSleepAsync
方法中,使用了Console.WriteLine(i)
語句,在每次循環(huán)中輸出當(dāng)前的計數(shù)值。這意味著無論是否等待異步方法的完成,都會輸出10次計數(shù)。
int b = await a;
語句只是等待異步方法的完成,并將返回的結(jié)果賦值給變量b
。它并不會觸發(fā)Console.WriteLine(i)
語句的執(zhí)行。
另一種:

a.Result表示等待異步方法TaskSleepAsync的完成,并獲取其返回的結(jié)果。通過使用a.Result,我們可以阻塞當(dāng)前線程,直到異步方法完成并返回結(jié)果。
a.Result和await都可以用于等待異步方法的完成并獲取其返回的結(jié)果,但它們有一些重要的區(qū)別。 阻塞 vs 非阻塞:使用a.Result會阻塞當(dāng)前線程,直到異步方法完成并返回結(jié)果。這意味著在等待期間,線程會被阻塞,無法執(zhí)行其他任務(wù)。而使用await關(guān)鍵字時,當(dāng)前線程會被釋放,可以執(zhí)行其他任務(wù),直到異步方法完成后再繼續(xù)執(zhí)行。
也就是說使用a.Result等待的話畫面會卡死,但是await不會