Thread、ThreadPool、Task、Parallel、Async和Await基本用法、區別以及弊端

  • 2019 年 10 月 3 日
  • 筆記

多執行緒的操作在程式中也是比較常見的,比如開啟一個執行緒執行一些比較耗時的操作(IO操作),而主執行緒繼續執行當前操作,不會造成主執行緒阻塞。執行緒又分為前台執行緒和後台執行緒,區別是:整個程式必須要運行完前台執行緒才會退出,而後台執行緒會在程式退出的時候結束掉。Thread默認創建的是前台執行緒,而ThreadPool和Task默認創建的是後台執行緒,Thread可以通過設置 IsBackground 屬性將執行緒設置為後台執行緒。

static void Main(string[] args)  {      Thread thread = new Thread(new ThreadStart(NoParameterMethod));      thread.Start();      Console.WriteLine("程式已經執行完成");  }    static void NoParameterMethod()  {      Thread.Sleep(1000);      Console.WriteLine("NoParameterMethod");  }

前台執行緒

效果:

static void Main(string[] args)  {      Thread thread = new Thread(new ThreadStart(NoParameterMethod))      {          IsBackground = true      };      thread.Start();      Console.WriteLine("程式已經執行完成");  }    static void NoParameterMethod()  {      Thread.Sleep(1000);      Console.WriteLine("NoParameterMethod");  }

後台執行緒

效果:

下面來說一下幾種開啟多執行緒的方法:

1、Thread

1.1 開啟一個執行緒,執行一個不帶參數的方法

static void Main(string[] args)  {      Thread thread = new Thread(new ThreadStart(NoParameterMethod));
//注意Start開啟執行緒之後,當前執行緒不是說一定會立馬執行
//而是說當前執行緒已經準備好被CPU調用,至於CPU什麼時候調用是需要看情況而定 thread.Start(); Console.WriteLine(
"程式已經執行完成"); } static void NoParameterMethod() {
//使當前執行緒停止1s
Thread.Sleep(1000); Console.WriteLine("NoParameterMethod"); }

1.2開啟一個執行緒,執行帶參數的方法

static void Main(string[] args)  {      Thread thread = new Thread(new ParameterizedThreadStart(ParameterMethod));      //要傳入的參數在Start的時候傳入      thread.Start("ParameterMethod");      Console.WriteLine("程式已經執行完成");  }  //參數類型必須為Object類型,方法只能有一個參數  //如果想傳入多個參數,可已將參數封裝進入一個類中  static void ParameterMethod(Object x) {      Thread.Sleep(1000);      Console.WriteLine(x);  }

2、ThreadPool

使用ThreadPool開啟一個執行緒

//無參    Thread.CurrentThread.ManagedThreadId是當前執行緒的唯一標識符  ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));  //有參  ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)), "參數");

ThreadPool是Thread的一個升級版,ThreadPool是從執行緒池中獲取執行緒,如果執行緒池中又空閑的元素,則直接調用,如果沒有才會創建,而Thread則是會一直創建新的執行緒,要知道開啟一個執行緒就算什麼事都不做也會消耗大約1m的記憶體,是非常浪費性能的,接下來我們寫一個例子來看一下二者的區別:

#region 使用Thread開啟100個執行緒  for (int i = 0; i < 100; i++)  {      (new Thread(new ThreadStart(() => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)))).Start();  }  #endregion

運行結果:

我們可以看到每一個主執行緒表示id都是不同的,也就是說使用Thread開啟執行緒每次都是新創建一個

#region 使用ThreadPool開啟100個執行緒  for (int i = 0; i < 100; i++)  {      ThreadPool.QueueUserWorkItem(new WaitCallback(obj => Console.WriteLine(Thread.CurrentThread.ManagedThreadId)));  }  #endregion

運行結果:

相信區別已經很明顯了,這裡我再說一下,執行緒池中一開始是沒有一個執行緒的,使用ThreadPool開啟一個執行緒之後,執行緒執行完畢,會加入到執行緒池中,後續需要再次開啟執行緒的時候查看執行緒池中有沒有空閑的執行緒,有則調用,沒有則創建,如此循環

二者之間還有一個區別,就是ThreadPool可以操控執行緒的狀態,比如等待執行緒完成,或者終止超時子執行緒操作

取消子執行緒操作

CancellationTokenSource cts = new CancellationTokenSource();  ThreadPool.QueueUserWorkItem(new WaitCallback(CanCancelMethod),cts.Token);  cts.Cancel();
Console.ReadKey();

static void CanCancelMethod(Object obj) {      CancellationToken ct = (CancellationToken)obj;      if (ct.IsCancellationRequested) {          Console.WriteLine("該執行緒已取消");      }      //就算ct.IsCancellationRequested為真,接下來的程式碼還是會執行      //因為該方法並沒有ruturn      Thread.Sleep(1000);      Console.WriteLine($"子執行緒{Thread.CurrentThread.ManagedThreadId}結束");  }

感覺這個取消子執行緒的方法和設置一個全局變數,然後通過判斷和更改全局變數的值,設置執行緒是否取消的效果一樣

ThreadPool的其他操作感興趣的可以自己搜索學一下,因為終止執行緒什麼操作都是比較麻煩的,關於ThreadPool就不再多說了

3、Task

Task和ThreadPool是一樣的,都是從執行緒池中取空閑的執行緒

 使用Task開啟一個執行緒

//方法1  使用Task的Run方法  Task.Run(()=> {      Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}已開啟");  });  //方法2   使用Task工廠類TaskFactory對象的StartNew方法  (new TaskFactory()).StartNew(() =>  {      Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}已開啟");  });

Run和StartNew方法都是返回一個Task類型的對象,代表當前開啟的執行緒,如果方法有返回值

//如果方法有返回值  Task<int> t1 = Task.Run<int>(() => {      return 1;  });  //通過t1.Result查看返回的結果  Console.WriteLine(t1.Result);

取消執行緒操作的話和ThreadPool取消執行緒操作一樣

//1s後自動取消執行緒  CancellationTokenSource cts = new CancellationTokenSource(1000);  //為取消執行緒註冊回調函數  cts.Token.Register(()=> {      Console.WriteLine("執行緒已取消");  });    Task.Run(()=> {      Console.WriteLine("開始執行");      Thread.Sleep(2000);      //判斷當前執行緒是否已被取消      if (cts.Token.IsCancellationRequested) {          Console.WriteLine("方法已結束");          return;      }      Console.WriteLine("執行緒繼續執行");  },cts.Token);

等待所有執行緒執行完畢

//存放所有執行緒  List<Task> lst = new List<Task>();  //開啟10個執行緒  for (int i = 0;i < 10;i++) {      lst.Add(Task.Run(()=> {          Thread.Sleep(new Random().Next(1,3000));          Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}");      }));  }  //等待所有執行緒執行完畢  Task.WaitAll(lst.ToArray());  Console.WriteLine("所有執行緒執行完畢");

等待任意一個先執行緒執行完畢

//存放所有執行緒  List<Task> lst = new List<Task>();  //開啟10個執行緒  for (int i = 0;i < 10;i++) {      lst.Add(Task.Run(()=> {          Thread.Sleep(new Random().Next(1,3000));          Console.WriteLine($"執行緒{Thread.CurrentThread.ManagedThreadId}");      }));  }  //等待任意執行緒執行完畢  Task.WaitAny(lst.ToArray());  Console.WriteLine("已有現成執行完畢");

對於Thread、ThreadPool和Task,如果要用多執行緒的話,優先使用Task,如果版本不支援Task,則考慮ThreadPool

4、Parallel

Parallel循環開啟多執行緒,並行任務,對於多執行緒開啟任務,開啟的順序都是不確定的

Parallel.Invoke()

Action[] action = new Action[] {      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),  };  Parallel.Invoke(action);

相當於

Action[] action = new Action[] {      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),  };  for (int i = 0; i < action.Length; i++)  {      Task.Run(action[i]);  }

Invoke時也可以進行一些配置,例如配置執行緒池中只能最多保持一個執行緒

Action[] action = new Action[] {      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),      ()=>Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}"),  };  Parallel.Invoke(new ParallelOptions()  {      MaxDegreeOfParallelism = 1  }, action);

運行結果:

Parallel.For()

//將迭代的結果保存起來  ParallelLoopResult plr =  Parallel.For(1, 10, (i) =>  {      Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");  });  Console.WriteLine(plr.IsCompleted);

相當於

for (int i = 1; i < 10; i++)  {      Task.Run(() =>      {          Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");      });  }

相對於循環Task.Run()更加簡潔

Parallel.ForEach()

方法和foreach類似,不過是採用的是非同步方式遍歷,要想被Parallel.ForEach()必須實現IEnumerable介面

Parallel.ForEach<String>(new List<String>() {      "a","b","c","d","e","f","g","h","i"  }, (str) =>  {      Console.WriteLine(str);  });

運行結果:

停止循環的方法

//將迭代的結果保存起來  ParallelLoopResult plr =  Parallel.For(1, 10, (i,state) =>  {      Console.WriteLine($"執行緒:{Thread.CurrentThread.ManagedThreadId}");      if (i==4) {          //結束          state.Break();      }  });  Console.WriteLine(plr.IsCompleted);

5、Async、Await

async和await關鍵字用來實現非同步編程,

async用來修飾方法,await用來調用方法,

await關鍵字必須出現在有async的方法中,await調用的方法可以不用async關鍵字修飾,但是返回值類型必須為Task<T>類型,

下面來說一下用法:

static void Main(string[] args)
{ Demo1(); Console.ReadKey(); }
static async void Demo1()
{
await Demo2(); } static async Task<int> Demo2()
{
return 1; }

await開啟非同步和Task開啟非同步還是有區別的

例如下面兩個例子

我們先用Task開啟非同步編程

static void Main(string[] args)  {      Console.WriteLine("主執行緒開始");      TaskDemo1();      Console.WriteLine("主執行緒結束");      Console.ReadKey();  }    static void TaskDemo1() {      Console.WriteLine("非同步開始");      Task.Run<int>(() =>      {          return TaskDemo2();      });      Console.WriteLine("非同步結束");  }    static int TaskDemo2()  {      Console.WriteLine("子執行緒開始");      Thread.Sleep(1000);      Console.WriteLine("子執行緒結束");      return 1;  }

我們這是可以大膽的猜測一下顯示的順尋

大致應該是:主執行緒開始==》非同步開始==》(子執行緒開始|非同步結束)=》(子執行緒開始|主執行緒結束)==》(子執行緒開始)=》子執行緒結束

運行結果:

果然和我們猜想的差不多,大致順序沒有變,接下來我們用async和await關鍵字開啟非同步

static void Main(string[] args)  {      Console.WriteLine("主執行緒開始");      AsyncDemo1();      Console.WriteLine("主執行緒結束");      Console.ReadKey();  }  static async void AsyncDemo1()  {      Console.WriteLine("非同步開始");      await AsyncDemo2();      Console.WriteLine("非同步結束");  }    static async Task<int> AsyncDemo2()  {      Console.WriteLine("子執行緒開始");      //當前子執行緒暫停1s      await Task.Delay(1000);      Console.WriteLine("子執行緒結束");      return 0;  }

 按理說順序也會是:主執行緒開始==》非同步開始==》(子執行緒開始|非同步結束)=》(子執行緒開始|主執行緒結束)==》(子執行緒開始)=》子執行緒結束

但事實是:

Task和async&await關鍵字的區別就此處

首先說一下梳理一下Task的執行過程(畫圖畫的很粗糙,重點是流程)

然後我們再來看一下async和await的執行過程

現在問題已經很清晰了,就是當主執行緒執行到await AsyncDemo2()時,會像是碰到了return語句一樣,退出當前方法(AsyncDemo1),將當前方法(AsyncDemo1)的後續執行語句交給子執行緒來執行,子執行緒會在執行完AsyncDemo2方法之後,返回過來執行AsyncDemo1方法。

這一點就是await與Task非同步編程的不同點


 

由 @悲夢 的評論,接下來給大家展示一個更好的例子來充分展現async&await和Task的區別

在上一步的基礎上,我們把TaskDemo1方法改為下面的樣子,接收一下返回值並輸出

static void TaskDemo1() {      Console.WriteLine("非同步開始");      //注意   使用Task<T>類型來接收返回值,並不會阻塞執行緒      Task<int> result = Task.Run<int>(() =>      {          return TaskDemo2();      });      //在此處輸出result.Result來查看新執行緒返回的結果時,才會阻塞執行緒      Console.WriteLine(result.Result);      Console.WriteLine("非同步結束");  }

運行到Console.WriteLine(result.Result)時,主執行緒會阻塞,等待子執行緒返回的結果然後輸出

然後看一下運行結果,和不接受返回值調用有很大的區別

因為,主執行緒阻塞了,所以順序是主執行緒最後結束,所以雖說是非同步編程,但是用不好的話反而會適得其所,不僅額外浪費多執行緒的開銷,而且還有沒得到程式應有的高效率

然後我們來看一下async,來接受一下返回值,並輸出,將AsyncDemo1改成如下的樣子

static async void AsyncDemo1()  {      Console.WriteLine("非同步開始");      int x = await AsyncDemo2();      Console.WriteLine(x);      Console.WriteLine("非同步結束");  }

可以發現,無論接收不接收await開啟非同步調用方法的返回值,都不會影響最終操作。async&await和Task的不同點還在上面那個流程圖中,在此程式中,運行至int x = await AsyncDemo2();時,主執行緒會返回main方法,而對x變數的賦值操作是由新開的執行緒來完成,輸出x變數也由新開的執行緒來完成,所以主執行緒不會造成阻塞,程式輸出的順序也不會變。