筆記 - C# 多執行緒與非同步

[筆記] C# 多執行緒與非同步

基本概念

多執行緒

  • 執行緒可以分工加速,但多建立一條執行緒,就會消耗約 1 MB 的記憶體來配置

  • 前臺執行緒

    • 程式必須執行完前臺執行緒才會結束退出
    • Thread 預設建立前臺執行緒
  • 後臺執行緒

    • 後臺執行緒在程式退出時就會結束
    • 後臺執行緒結束時,程式並不會結束退出
    • ThreadPool、Task、Parallel 預設建立後臺執行緒

非同步

  • 非同步目的在於增加產能而非提高效能

    • 當程式在等待時,可以先去執行另一項程式,不浪費等待時間
  • 非同步不等於多執行緒

    • 多執行緒的精神為分工加速,建立多個執行緒個別處理
    • 非同步的重點在於允許執行緒在等待時間先處理其他工作
  • 非同步對 I/O 相關工作較有效

    • I/O 相關工作例如 : 呼叫 API、存取資料庫
    • 要等待 I/O 回應的等待時間,可以先處理其他工作
    • 而大量消耗 CPU 的重度運算,並無等待時間,使用多執行緒較有效率

四種建立執行緒的方法

  • 歷史演變由上而下

    • Thread
    • ThreadPool
    • Task
    • Parallel

==TPL ( Task Parallel Library ) 是 .NET 4.0 中增加的平行運算函式庫,其中包含了 Task 類別與 Parallel 類別==

Thread

  • 使用 System.Threading.Thread 類別

  • 執行不帶參數的方法

    • 開啟執行緒

      1
      2
      Thread thread = new Thread(new ThreadStart(Method));
      thread.Start(); //並不是馬上執行,而是準備好被 CPU 執行,甚麼時候執行視情況而定
    • 方法

      1
      2
      3
      4
      5
      public void Method()
      {
      Thread.Sleep(1000); //單位為毫秒
      Console.WriteLine("thread done");
      }
  • 執行帶參數的方法

    • 開啟執行緒

      1
      2
      Thread thread = new Thread(new ParameterizedThreadStart(Method));
      thread.Start("thread done");
    • 方法

      1
      2
      3
      4
      5
      public void Method(string str)
      {
      Thread.Sleep(1000);
      Console.WriteLine(str);
      }

ThreadPool

  • 使用 System.Threading.ThreadPool 類別

  • 為 Thread 的升級版

  • 執行不帶參數的方法

    1
    ThreadPool.QueueUserWorkItem(new WaitCallback(x => Console.WriteLine("thread done")));
  • 執行帶參數的方法

    1
    ThreadPool.QueueUserWorkItem(new WaitCallback(x => Console.WriteLine(x)), "thread done");
  • 與 Thread 的差別 :

    • 建立執行緒

      • Thread : 每次都建立一個新的執行緒
      • ThreadPool : 會查看執行緒池,若無空閒的執行緒才建立,剛開始執行緒池是沒有執行緒的,當 ThreadPool 建立一個執行緒後,此執行緒才會加入執行緒池
    • 操控執行緒的狀態

      • ThreadPool 可以操控執行緒的狀態,例如 : 等待執行緒完成、中止超時的執行緒等,Thread 則不行

Task

  • 使用 System.Threading.Tasks.Task 類別

  • 與 ThreadPool 是一樣使用執行緒池的

  • 兩種建立方式

    • 使用 Task 的 Run 方法

      1
      2
      3
      Task.Run(()=> {
      Console.WriteLine("thread done");
      });
    • 使用 TaskFactory 物件的 StartNew 方法

      1
      2
      3
      4
      (new TaskFactory()).StartNew(() =>
      {
      Console.WriteLine("thread done");
      });
  • 方法有返回值

    1
    2
    3
    4
    5
    Task<string> task = Task.Run<string>(() => {
    return "thread done";
    });

    Console.WriteLine(task.Result);
  • 取消超時執行緒

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //1秒後自動取消執行緒
    CancellationTokenSource cts = new CancellationTokenSource(1000);

    cts.Token.Register(()=> {
    Console.WriteLine("thread cancle");
    });

    Task.Run(()=> {
    Console.WriteLine("thread start");
    Thread.Sleep(2000);

    if (cts.Token.IsCancellationRequested) {
    Console.WriteLine("thread stop");
    return;
    }

    Console.WriteLine("thread done");
    }, cts.Token);
  • 等待所有執行緒執行完畢

    1
    Task.WaitAll(Task1, Task2, Task3);
  • 等待任意一個執行緒執行完畢

    1
    Task.WaitAny(Task1, Task2, Task3);
  • 與 ThreadPool 差別在於 Task 在多核 CPU 時效能較優,因為 ThreadPool 使用的執行緒池是全域,會造成資源共享的競爭,且 Task 提供較豐富的 API 方法

Parallel

  • 使用 System.Threading.Tasks.Parallel 類別

  • Parallel.Invoke()

    1
    2
    3
    4
    5
    6
    Action[] action = new Action[] {
    ()=>Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}"),
    ()=>Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}"),
    };
    Parallel.Invoke(action);
  • Parallel.For()

1
2
3
4
5
ParallelLoopResult plr = Parallel.For(1, 10, (i) =>
{
Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine(plr.IsCompleted);
  • Parallel.ForEach()
1
2
3
4
5
6
Parallel.ForEach<String>(new List<String>() {
"a","b","c","d","e","f","g","h","i"
}, (str) =>
{
Console.WriteLine(str);
});
  • 與 Task 差別在於執行緒的數量控制較為簡單

使用非同步的方法

async/await

  • 為非同步的修飾詞

    • async 用來修飾方法,await 用來呼叫方法
    • await 必須出現在有 async 修飾的方法中
    • await 呼叫的方法可以不用 async 修飾,但返回值必須為 Task<T> 型別
  • async 像病毒一樣會傳染

    • 當方法前加上 async 後,裡面若呼叫外部方法必須加上 await
    • 此設計是為了避免同步與非同步寫法混用
  • 將同步程式重構為非同步

    • 建議採由下而上 ( Bottom-Up ) 的策略
    • 若由上開始,所有呼叫到的方法都必須更改
tags: 筆記 C# 多執行緒 非同步
Author: Kenny Li
Link: https://kennyliblog.nctu.me/2020/12/05/Csharp-thread-and-Async/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.