python異步和多線程區別,C#多線程和異步(二)——Task和async/await詳解(轉載)

 2023-10-06 阅读 27 评论 0

摘要:一、什么是異步   同步和異步主要用于修飾方法。當一個方法被調用時,調用者需要等待該方法執行完畢并返回才能繼續執行,我們稱這個方法是同步方法;當一個方法被調用時立即返回,并獲取一個線程執行該方法內部的業務,調用者不用等待該方法

一、什么是異步

  同步和異步主要用于修飾方法。當一個方法被調用時,調用者需要等待該方法執行完畢并返回才能繼續執行,我們稱這個方法是同步方法;當一個方法被調用時立即返回,并獲取一個線程執行該方法內部的業務,調用者不用等待該方法執行完畢,我們稱這個方法為異步方法。

python異步和多線程區別。  異步的好處在于非阻塞(調用線程不會暫停執行去等待子線程完成),因此我們把一些不需要立即使用結果、較耗時的任務設為異步執行,可以提高程序的運行效率。net4.0在ThreadPool的基礎上推出了Task類,微軟極力推薦使用Task來執行異步任務,現在C#類庫中的異步方法基本都用到了Task;net5.0推出了async/await,讓異步編程更為方便。本篇主要介紹Task、async/await相關的內容,其他異步操作的方式會在下一篇介紹。

回到頂部

二、Task介紹

  Task是在ThreadPool的基礎上推出的,我們簡單了解下ThreadPool。ThreadPool中有若干數量的線程,如果有任務需要處理時,會從線程池中獲取一個空閑的線程來執行任務,任務執行完畢后線程不會銷毀,而是被線程池回收以供后續任務使用。當線程池中所有的線程都在忙碌時,又有新任務要處理時,線程池才會新建一個線程來處理該任務,如果線程數量達到設置的最大值,任務會排隊,等待其他任務釋放線程后再執行。線程池能減少線程的創建,節省開銷,看一個ThreadPool的栗子吧

復制代碼
        static void Main(string[] args){for (int i = 1; i <=10; i++){//ThreadPool執行任務ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {Console.WriteLine($"第{obj}個執行任務");}),i);}Console.ReadKey();}
復制代碼

spring異步。  上邊的代碼通過ThreadPool執行了10個任務,執行結果為:

?  ThreadPool相對于Thread來說可以減少線程的創建,有效減小系統開銷;但是ThreadPool不能控制線程的執行順序,我們也不能獲取線程池內線程取消/異常/完成的通知,即我們不能有效監控和控制線程池中的線程。

1 Task創建和運行

  我們知道了ThreadPool的弊端:我們不能控制線程池中線程的執行順序,也不能獲取線程池內線程取消/異常/完成的通知。net4.0在ThreadPool的基礎上推出了Task,Task擁有線程池的優點,同時也解決了使用線程池不易控制的弊端。

首先看一下怎么去創建并運行一個Task,Task的創建和執行方式有如下三種:

復制代碼
        static void Main(string[] args){//1.new方式實例化一個Task,需要通過Start方法啟動Task task = new Task(() =>{Thread.Sleep(100);Console.WriteLine($"hello, task1的線程ID為{Thread.CurrentThread.ManagedThreadId}");});task.Start();//2.Task.Factory.StartNew(Action action)創建和啟動一個TaskTask task2 = Task.Factory.StartNew(() =>{Thread.Sleep(100);Console.WriteLine($"hello, task2的線程ID為{ Thread.CurrentThread.ManagedThreadId}");});//3.Task.Run(Action action)將任務放在線程池隊列,返回并啟動一個TaskTask task3 = Task.Run(() =>{Thread.Sleep(100);Console.WriteLine($"hello, task3的線程ID為{ Thread.CurrentThread.ManagedThreadId}");});Console.WriteLine("執行主線程!");Console.ReadKey();}
復制代碼

執行結果如下:

  我們看到先打印"執行主線程",然后再打印各個任務,說明了Task不會阻塞主線程。上邊的栗子Task都沒有返回值,我們也可以創建有返回值的Task<TResult>,用法和沒有返回值的基本一致,我們簡單修改一下上邊的栗子,代碼如下:

復制代碼
        static void Main(string[] args){1.new方式實例化一個Task,需要通過Start方法啟動Task<string> task = new Task<string>(() =>{return $"hello, task1的ID為{Thread.CurrentThread.ManagedThreadId}";});task.Start();2.Task.Factory.StartNew(Func func)創建和啟動一個TaskTask<string> task2 =Task.Factory.StartNew<string>(() =>{return $"hello, task2的ID為{ Thread.CurrentThread.ManagedThreadId}";});3.Task.Run(Func func)將任務放在線程池隊列,返回并啟動一個TaskTask<string> task3= Task.Run<string>(() =>{return $"hello, task3的ID為{ Thread.CurrentThread.ManagedThreadId}";});Console.WriteLine("執行主線程!");Console.WriteLine(task.Result);Console.WriteLine(task2.Result);Console.WriteLine(task3.Result);Console.ReadKey();}
復制代碼

  注意task.Resut獲取結果時會阻塞線程,即如果task沒有執行完成,會等待task執行完成獲取到Result,然后再執行后邊的代碼,程序運行結果如下:? ?

  上邊的所有栗子中Task的執行都是異步的,不會阻塞主線程。有些場景下我們想讓Task同步執行怎么辦呢?Task提供了??task.RunSynchronously()用于同步執行Task任務,代碼如下:

復制代碼
        static void Main(string[] args){Task task = new Task(() =>{Thread.Sleep(100);Console.WriteLine("執行Task結束!");});//同步執行,task會阻塞主線程task.RunSynchronously();Console.WriteLine("執行主線程結束!");Console.ReadKey();}
復制代碼

執行結果如下:

?2 Task的阻塞方法(Wait/WaitAll/WaitAny)

1 Thread阻塞線程的方法

  使用Thread時,我們知道用thread.Join()方法即可阻塞主線程。看一個例子:

復制代碼
        static void Main(string[] args){Thread th1 = new Thread(() => {Thread.Sleep(500);Console.WriteLine("線程1執行完畢!");});th1.Start();Thread th2 = new Thread(() => {Thread.Sleep(1000);Console.WriteLine("線程2執行完畢!");});th2.Start();//阻塞主線程th1.Join();th2.Join();Console.WriteLine("主線程執行完畢!");Console.ReadKey();}
復制代碼

  如果注釋掉兩個Join,執行結果是:先打印【主線程執行完畢】,而添加兩個Join方法后執行結果如下,實現了線程阻塞:

2 Task的Wait/WaitAny/WaitAll方法

?  Thread的Join方法可以阻塞調用線程,但是有一些弊端:①如果我們要實現很多線程的阻塞時,每個線程都要調用一次Join方法;②如果我們想讓所有的線程執行完畢(或者任一線程執行完畢)時,立即解除阻塞,使用Join方法不容易實現。Task提供了??Wait/WaitAny/WaitAll??方法,可以更方便地控制線程阻塞。

??task.Wait()??表示等待task執行完畢,功能類似于thead.Join();??Task.WaitAll(Task[] tasks)??表示只有所有的task都執行完成了再解除阻塞;?Task.WaitAny(Task[] tasks)?表示只要有一個task執行完畢就解除阻塞,看一個栗子:?

復制代碼
        static void Main(string[] args){Task task1 = new Task(() => {Thread.Sleep(500);Console.WriteLine("線程1執行完畢!");});task1.Start();Task task2 = new Task(() => {Thread.Sleep(1000);Console.WriteLine("線程2執行完畢!");});task2.Start();//阻塞主線程。task1,task2都執行完畢再執行主線程//執行【task1.Wait();task2.Wait();】可以實現相同功能Task.WaitAll(new Task[]{ task1,task2});Console.WriteLine("主線程執行完畢!");Console.ReadKey();}
復制代碼

  執行結果如下:

  如果將栗子中的WaitAll換成WaitAny,那么任一task執行完畢就會解除線程阻塞,執行結果是:先打印【線程1執行完畢】,然后打印【主線程執行完畢】,最后打印【線程2執行完畢】

?3 Task的延續操作(WhenAny/WhenAll/ContinueWith)

  上邊的Wait/WaitAny/WaitAll方法返回值為void,這些方法單純的實現阻塞線程。我們現在想讓所有task執行完畢(或者任一task執行完畢)后,開始執行后續操作,怎么實現呢?這時就可以用到WhenAny/WhenAll方法了,這些方法執行完成返回一個task實例。??task.WhenAll(Task[] tasks)??表示所有的task都執行完畢后再去執行后續的操作,?task.WhenAny(Task[] tasks)??表示任一task執行完畢后就開始執行后續操作。看一個栗子:

復制代碼
        static void Main(string[] args){Task task1 = new Task(() => {Thread.Sleep(500);Console.WriteLine("線程1執行完畢!");});task1.Start();Task task2 = new Task(() => {Thread.Sleep(1000);Console.WriteLine("線程2執行完畢!");});task2.Start();//task1,task2執行完了后執行后續操作Task.WhenAll(task1, task2).ContinueWith((t) => {Thread.Sleep(100);Console.WriteLine("執行后續操作完畢!");});Console.WriteLine("主線程執行完畢!");Console.ReadKey();}
復制代碼

  執行結果如下,我們看到WhenAll/WhenAny方法不會阻塞主線程,當使用WhenAll方法時所有的task都執行完畢才會執行后續操作;如果把栗子中的WhenAll替換成WhenAny,則只要有一個線程執行完畢就會開始執行后續操作,這里不再演示。

?  上邊的栗子也可以通過?Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction)??和?Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction)?來實現 ,修改上邊栗子代碼如下,執行結果不變。

復制代碼
       static void Main(string[] args){Task task1 = new Task(() => {Thread.Sleep(500);Console.WriteLine("線程1執行完畢!");});task1.Start();Task task2 = new Task(() => {Thread.Sleep(1000);Console.WriteLine("線程2執行完畢!");});task2.Start();//通過TaskFactroy實現Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>{Thread.Sleep(100);Console.WriteLine("執行后續操作");});Console.WriteLine("主線程執行完畢!");Console.ReadKey();}
復制代碼

?4 Task的任務取消(CancellationTokenSource)

1 Thread取消任務執行

  在Task前我們執行任務采用的是Thread,Thread怎么取消任務呢?一般流程是:設置一個變量來控制任務是否停止,如設置一個變量isStop,然后線程輪詢查看isStop,如果isStop為true就停止,代碼如下:

復制代碼
        static void Main(string[] args){bool isStop = false;int index = 0;//開啟一個線程執行任務Thread th1 = new Thread(() =>{while (!isStop){Thread.Sleep(1000);Console.WriteLine($"第{++index}次執行,線程運行中...");}});th1.Start();//五秒后取消任務執行Thread.Sleep(5000);isStop = true;Console.ReadKey();}
復制代碼

2 Task取消任務執行

  Task中有一個專門的類?CancellationTokenSource??來取消任務執行,還是使用上邊的例子,我們修改代碼如下,程序運行的效果不變。

復制代碼
        static void Main(string[] args){CancellationTokenSource source = new CancellationTokenSource();int index = 0;//開啟一個task執行任務Task task1 = new Task(() =>{while (!source.IsCancellationRequested){Thread.Sleep(1000);Console.WriteLine($"第{++index}次執行,線程運行中...");}});task1.Start();//五秒后取消任務執行Thread.Sleep(5000);//source.Cancel()方法請求取消任務,IsCancellationRequested會變成truesource.Cancel();Console.ReadKey();}
復制代碼

  ?CancellationTokenSource的功能不僅僅是取消任務執行,我們可以使用??source.CancelAfter(5000)??實現5秒后自動取消任務,也可以通過?source.Token.Register(Action action)??注冊取消任務觸發的回調函數,即任務被取消時注冊的action會被執行。 看一個栗子:

復制代碼
        static void Main(string[] args){CancellationTokenSource source = new CancellationTokenSource();//注冊任務取消的事件source.Token.Register(() =>{Console.WriteLine("任務被取消后執行xx操作!");});int index = 0;//開啟一個task執行任務Task task1 = new Task(() =>{while (!source.IsCancellationRequested){Thread.Sleep(1000);Console.WriteLine($"第{++index}次執行,線程運行中...");}});task1.Start();//延時取消,效果等同于Thread.Sleep(5000);source.Cancel();source.CancelAfter(5000);Console.ReadKey();}
復制代碼

  執行結果如下,第5次執行在取消回調后打印,這是因為,執行取消的時候第5次任務已經通過了while()判斷,任務已經執行中了:

  ?最后看上一篇跨線程的栗子,點擊按鈕啟動一個任務,給tetxtbox賦值,我們把Thread改成Task,代碼如下:

復制代碼
    public partial class Form1 : Form{public Form1(){InitializeComponent();}private void mySetValueBtn_Click(object sender, EventArgs e){Task.Run(() =>{Action<int> setValue = (i) => { myTxtbox.Text = i.ToString(); };for (int i = 0; i < 1000000; i++){myTxtbox.Invoke(setValue,i);}});}}
復制代碼

  運行界面如下,賦值的task不會阻塞UI線程:

回到頂部

三、異步方法(async/await)

  在C#5.0中出現的?async和await?,讓異步編程變得更簡單。我們看一個獲取文件內容的栗子:

復制代碼
    class Program{static void Main(string[] args){string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;//調用同步方法//string content = GetContent(Environment.CurrentDirectory + @"/test.txt");Console.WriteLine(content);Console.ReadKey();}//異步讀取文件內容async static Task<string> GetContentAsync(string filename){FileStream fs = new FileStream(filename, FileMode.Open);var bytes = new byte[fs.Length];//ReadAync方法異步讀取內容,不阻塞線程Console.WriteLine("開始讀取文件");int len = await fs.ReadAsync(bytes, 0, bytes.Length);string result = Encoding.UTF8.GetString(bytes);return result;}//同步讀取文件內容static string GetContent(string filename){FileStream fs = new FileStream(filename, FileMode.Open);var bytes = new byte[fs.Length];//Read方法同步讀取內容,阻塞線程int len =  fs.Read(bytes, 0, bytes.Length);string result = Encoding.UTF8.GetString(bytes);return result;}}
復制代碼

  test.txt內容是【hello world!】執行結果為:

  上邊的栗子也寫出了同步讀取的方式,將main函數中的注釋去掉即可同步讀取文件內容。我們可以看到異步讀取代碼和同步讀取代碼基本一致。async/await讓異步編碼變得更簡單,我們可以像寫同步代碼一樣去寫異步代碼。注意一個小問題:異步方法中方法簽名返回值為Task<TResult>,代碼中的返回值為TResult。上邊栗子中GetContentAsync的簽名返回值為Task<string>,而代碼中返回值為string。牢記這一細節對我們分析異步代碼很有幫助。通過上邊的栗子我們知道aysnc/await的用法十分簡單,那么aynsc標記的異步方法是怎樣執行的呢?什么時候創建的任務線程?再看一個簡單的栗子:

復制代碼
    class Program{static void Main(string[] args){Console.WriteLine(">>>>>>>>>>>>>>>>主線程啟動");Task<string> task = GetStringAsync1();Console.WriteLine("<<<<<<<<<<<<<<<<主線程結束");Console.WriteLine($"GetStringAsync1執行結果:{task.Result}");}static async Task<string> GetStringAsync1(){Console.WriteLine(">>>>>>>>GetStringAsync1方法啟動");string str = await GetStringAsync2();Console.WriteLine("<<<<<<<<GetStringAsync1方法結束");return str;}static async Task<string> GetStringAsync2(){Console.WriteLine(">>>>>>>>GetStringAsync2方法啟動");string str = await GetStringFromTask();Console.WriteLine("<<<<<<<<GetStringAsync2方法結束");return str;}static Task<string> GetStringFromTask(){Console.WriteLine(">>>>GetStringFromTask方法啟動");Task<string> task = new Task<string>(() =>{Console.WriteLine(">>任務線程啟動");Thread.Sleep(1000);Console.WriteLine("<<任務線程結束");return "hello world";});task.Start();Console.WriteLine("<<<<GetStringFromTask方法結束");return task;}}
復制代碼

  使用async/await時,主線程和執行同步代碼一樣,是一路執行到底的,執行到await時也沒有返回main函數,而是創建了一個空閑的任務后繼續往下執行,當所有的調用的方法都執行完畢后再返回main函數。主線程執行到task.Start()才創建任務線程,真正開始執行異步任務。當異步方法async? Task<T>A()需要另一個異步方法async Task<T>B()的結果時,A()會等待B()完成異步任務再繼續向下執行。程序執行結果如下:

  小結:到這里Task,async/await的簡單使用已經基本結束了,一些高級特性等到工作遇到了再去研究。通過上邊的介紹,我們知道async/await是基于Task的,而Task是對ThreadPool的封裝改進,主要是為了更有效的控制線程池中的線程(ThreadPool中的線程,我們很難通過代碼控制其執行順序,任務延續和取消等等);ThreadPool基于Thread的,主要目的是減少Thread創建數量和管理Thread的成本。async/await Task是C#中更先進的,也是微軟大力推廣的特性,我們在開發中可以嘗試使用Task來替代Thread/ThreadPool,處理本地IO和網絡IO任務是盡量使用async/await來提高任務執行效率。

轉載自:https://www.cnblogs.com/wyy1234/

轉載請注明原始地址,感謝!

轉載于:https://www.cnblogs.com/candyzhmm/p/10785252.html

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/4/119011.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息