- 1 多執行緒
- 1.1 簡介
- 1.1.1 程序&執行緒
- 1.1.2 執行緒優缺點
- 1.1.3 主執行緒
- 1.2 執行緒生命週期
- 1.3 常用屬性和方法
- 1.4 建立執行緒
- 1.4.1 System.Threading.Thread
- 1.4.1.1 不帶引數處理
- 1.4.1.2 帶引數處理
- 1.4.1.3 不用new ThreadStart
- 1.4.2 ThreadPool
- 1.4.3 System.Threading.Tasks.Task
- 1.4.3.1 Task與ThreadPool區別
- 1.4.3.2 Task 的使用方式
- 1.4.3.3 Start 和 Wait
- 1.4.3.4 示例
- 1.4.4 async/await
- 1.4.4.1 解釋分析
- 1.4.4.2 非同步方法的異常處理
- 1.4.4.3 await 返回
- 1.4.4.4 示例分析
- 1.4.5 System.Threading.Tasks.Parallel
- 1.4.1 System.Threading.Thread
- 1.1 簡介
1 多執行緒
1.1 簡介
1.1.1 程序&執行緒
執行緒與程序區別:
- 程序(
Process
)是Windows系統中的一個基本概念,它包含著一個執行程式所需要的資源。一個正在執行的應用程式在作業系統中被視為一個程序,程序可以包括一個或多個執行緒。執行緒是作業系統分配處理器時間的基本單元,在程序中可以有多個執行緒同時執行程式碼。程序之間是相對獨立的,一個程序無法訪問另一個程序的資料(除非利用分散式計算方式),一個程序執行的失敗也不會影響其他程序的執行,Windows系統就是利用程序把工作劃分為多個獨立的區域的。程序可以理解為一個程式的基本邊界。是應用程式的一個執行例程,是應用程式的一次動態執行過程。 - 執行緒(
Thread
)是程序中的基本執行單元,是作業系統分配CPU時間的基本單位,一個程序可以包含若干個執行緒,在程序入口執行的第一個執行緒
被視為這個程序的主執行緒
。在.NET
應用程式中,都是以Main()
方法作為入口的,當呼叫此方法時系統就會自動建立一個主執行緒。執行緒主要是由CPU暫存器、呼叫棧和執行緒本地儲存器(Thread Local Storage,TLS)組成的。CPU暫存器主要記錄當前所執行執行緒的狀態,呼叫棧主要用於維護執行緒所呼叫到的記憶體與資料,TLS主要用於存放執行緒的狀態資訊。
1.1.2 執行緒優缺點
多執行緒的優點:可以同時完成多個任務;可以使程式的響應速度更快;可以讓佔用大量處理時間的任務或當前沒有進行處理的任務定期將處理時間讓給別的任務;可以隨時停止任務;可以設定每個任務的優先順序以最佳化程式效能。
缺點:
- 執行緒也是程式,所以執行緒需要佔用記憶體,執行緒越多,佔用記憶體也越多。
- 多執行緒需要協調和管理,所以需要佔用CPU時間以便跟蹤執行緒。
- 執行緒之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
- 執行緒太多會導致控制太複雜,最終可能造成很多程式缺陷。
1.1.3 主執行緒
在 C#
中,System.Threading.Thread
類用於執行緒的工作。建立並訪問多執行緒應用程式中的單個執行緒。程序中第一個被執行的執行緒稱為主執行緒
。
當 C# 程式開始執行時,主執行緒自動建立。使用 Thread
類建立的執行緒被主執行緒的子執行緒呼叫。可以使用 Thread 類的 CurrentThread
屬性訪問執行緒。
下面的程式演示了主執行緒的執行:
using System;
using System.Threading;
namespace MultithreadingApplication
{
class MainThreadProgram
{
static void Main(string[] args)
{
Thread th = Thread.CurrentThread;
th.Name = "MainThread";
Console.WriteLine("This is {0}", th.Name);
Console.ReadKey();
}
}
}
結果:
This is MainThread
1.2 執行緒生命週期
執行緒生命週期開始於 System.Threading.Thread
類的物件被建立時,結束於執行緒被終止或完成執行時。
下面列出了執行緒生命週期中的各種狀態:
未啟動狀態
:當執行緒例項被建立但Start
方法未被呼叫時的狀況。就緒狀態
:當執行緒準備好執行並等待 CPU 週期時的狀況。不可執行狀態
:下面的幾種情況下執行緒是不可執行的:
已經呼叫 Sleep 方法
已經呼叫 Wait 方法
透過 I/O 操作阻塞死亡狀態
:當執行緒已完成執行或已中止時的狀況。
1.3 常用屬性和方法
下表列出了 Thread 類的一些常用的 屬性:
屬性 | 描述 |
---|---|
CurrentContext | 獲取執行緒正在其中執行的當前上下文 |
CurrentCulture | 獲取或設定當前執行緒的區域性 |
CurrentPrincipal | 獲取或設定執行緒的當前負責人(對基於角色的安全性而言) |
CurrentThread | 獲取當前正在執行的執行緒 |
CurrentUICulture | 獲取或設定資源管理器使用的當前區域性以便在執行時查詢區域性特定的資源 |
ExecutionContext | 獲取一個 ExecutionContext 物件,該物件包含有關當前執行緒的各種上下文的資訊 |
IsAlive | 獲取一個值,該值指示當前執行緒的執行狀態 |
IsBackground | 獲取或設定一個值,該值指示某個執行緒是否為後臺執行緒 |
IsThreadPoolThread | 獲取一個值,該值指示執行緒是否屬於託管執行緒池 |
ManagedThreadId | 獲取當前託管執行緒的唯一識別符號 |
Name | 獲取或設定執行緒的名稱 |
Priority | 獲取或設定一個值,該值指示執行緒的排程優先順序 |
ThreadState | 獲取一個值,該值包含當前執行緒的狀態 |
下表列出了 Thread 類的一些常用的 方法:
方法名 | 描述 |
---|---|
public void Abort() | 在呼叫此方法的執行緒上引發 ThreadAbortException,以開始終止此執行緒的過程。呼叫此方法通常會終止執行緒 |
public static LocalDataStoreSlot AllocateDataSlot() | 在所有的執行緒上分配未命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public static LocalDataStoreSlot AllocateNamedDataSlot( string name) | 在所有執行緒上分配已命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public static void BeginCriticalRegion() | 通知主機執行將要進入一個程式碼區域,在該程式碼區域內執行緒中止或未經處理的異常的影響可能會危害應用程式域中的其他任務 |
public static void BeginThreadAffinity() | 通知主機託管程式碼將要執行依賴於當前物理作業系統執行緒的標識的指令 |
public static void EndCriticalRegion() | 通知主機執行將要進入一個程式碼區域,在該程式碼區域內執行緒中止或未經處理的異常僅影響當前任務 |
public static void EndThreadAffinity() | 通知主機託管程式碼已執行完依賴於當前物理作業系統執行緒的標識的指令 |
public static void FreeNamedDataSlot(string name) | 為程序中的所有執行緒消除名稱與槽之間的關聯。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public static Object GetData( LocalDataStoreSlot slot ) | 在當前執行緒的當前域中從當前執行緒上指定的槽中檢索值。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public static AppDomain GetDomain() | 返回當前執行緒正在其中執行的當前域 |
public static AppDomain GetDomainID() | 返回唯一的應用程式域識別符號 |
public static LocalDataStoreSlot GetNamedDataSlot( string name ) | 查詢已命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public void Interrupt() | 中斷處於 WaitSleepJoin 執行緒狀態的執行緒 |
public void Join() | 在繼續執行標準的 COM 和 SendMessage 訊息泵處理期間,阻塞呼叫執行緒,直到某個執行緒終止為止。此方法有不同的過載形式 |
public static void MemoryBarrier() | 按如下方式同步記憶體存取:執行當前執行緒的處理器在對指令重新排序時,不能採用先執行 MemoryBarrier 呼叫之後的記憶體存取,再執行 MemoryBarrier 呼叫之前的記憶體存取的方式 |
public static void ResetAbort() | 取消為當前執行緒請求的 Abort |
public static void SetData( LocalDataStoreSlot slot, Object data ) | 在當前正在執行的執行緒上為此執行緒的當前域在指定槽中設定資料。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位 |
public void Start() | 開始一個執行緒 |
public static void Sleep( int millisecondsTimeout ) | 讓執行緒暫停一段時間 |
public static void SpinWait( int iterations ) | 導致執行緒等待由 iterations 引數定義的時間量 |
public static byte VolatileRead( ref byte address ) public static double VolatileRead( ref double address ) public static int VolatileRead( ref int address ) public static Object VolatileRead( ref Object address ) |
讀取欄位值。無論處理器的數目或處理器快取的狀態如何,該值都是由計算機的任何處理器寫入的最新值。此方法有不同的過載形式。這裡只給出了一些形式 |
public static void VolatileWrite( ref byte address, byte value ) public static void VolatileWrite( ref double address, double value ) public static void VolatileWrite( ref int address, int value ) public static void VolatileWrite( ref Object address, Object value ) |
立即向欄位寫入一個值,以使該值對計算機中的所有處理器都可見。此方法有不同的過載形式。這裡只給出了一些形式 |
public static bool Yield() | 導致呼叫執行緒執行準備好在當前處理器上執行的另一個執行緒。由作業系統選擇要執行的執行緒 |
1.4 建立執行緒
1.4.1 System.Threading.Thread
執行緒是透過擴充套件 Thread 類建立的。擴充套件的 Thread 類呼叫 Start() 方法來開始子執行緒的執行。
1.4.1.1 不帶引數處理
下面的程式演示了這個概念:
using System;
using System.Threading;
namespace MultithreadingApplication
{
class ThreadCreationProgram
{
public static void CallToChildThread()
{
Console.WriteLine("Child thread starts");
}
static void Main(string[] args)
{
Console.WriteLine("In Main: Creating the Child thread");
ThreadStart childref = new ThreadStart(CallToChildThread);
Thread childThread = new Thread(childref);
childThread.Start();
Console.ReadKey();
}
}
}
結果:
In Main: Creating the Child thread
Child thread starts
1.4.1.2 帶引數處理
執行緒函式透過委託傳遞,可以不帶引數,也可以帶引數(只能有一個引數
),可以用一個類或結構體封裝引數
using System;
using System.Threading;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(TestMethod));
Thread t2 = new Thread(new ParameterizedThreadStart(TestMethod));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start("hello");
Console.ReadKey();
}
public static void TestMethod()
{
Console.WriteLine("不帶引數的執行緒函式");
}
public static void TestMethod(object data)
{
string datastr = data as string;
Console.WriteLine("帶引數的執行緒函式,引數為:{0}", datastr);
}
}
}
1.4.1.3 不用new ThreadStart
c#2.0 後可以直接傳入方法而不用寫 new ThreadStart
,但是需要注意,如果 CallToChildThread 方法沒有過載
,可以直接用下面的簡化寫法,如果有過載就需要先用 new ThreadStart
建立執行緒入口,不然編輯器會報“方法或屬性呼叫不明確”錯誤
using System.Threading.Tasks;
using System.Threading;
namespace ConsoleApp
{
class Program
{
public static void CallToChildThread()
{
Console.WriteLine("Child thread starts");
}
static void Main(string[] args)
{
//ThreadStart childref = new ThreadStart(CallToChildThread);
Console.WriteLine("In Main: Creating the Child thread");
Thread childThread = new Thread(CallToChildThread);
childThread.Start();
Console.ReadKey();
}
}
}
1.4.2 ThreadPool
ThreadPool 由 .Net
自己管理, 只需要把需要處理的方法寫好, 然後交個.Net Framework, 後續只要方法執行完畢, 則自動退出。ThreadPool 提供了一種簡單而高效的方式來管理執行緒池,從而可以方便地建立和管理多執行緒應用程式。使用執行緒池可以顯著降低建立和銷燬執行緒的開銷,並且有助於更好地管理系統資源。
using System;
using System.Threading;
class Program
{
// 工作方法,執行緒池中的執行緒將執行這個方法。
static void ThreadPoolCallback(object state)
{
// 將傳遞的狀態資訊轉換為字串並列印。
Console.WriteLine("Thread ID: {0}, State: {1}", Thread.CurrentThread.ManagedThreadId, state);
// 模擬一些工作負載。
Thread.Sleep(2000); // 休眠2秒
// 列印完成資訊。
Console.WriteLine("Thread ID: {0} completed.", Thread.CurrentThread.ManagedThreadId);
}
static void Main()
{
// 要排隊到執行緒池中的任務數量。
int numberOfTasks = 5;
// 排隊任務到執行緒池中。
for (int i = 0; i < numberOfTasks; i++)
{
// 使用 ThreadPool.QueueUserWorkItem 方法將任務排隊到執行緒池中。
// 這個方法接受一個 WaitCallback 委託和一個狀態物件作為引數。
ThreadPool.QueueUserWorkItem(ThreadPoolCallback, i);
}
// 防止主執行緒在所有任務完成之前退出。
Console.WriteLine("Main thread waiting for ThreadPool tasks to complete...");
Thread.Sleep(15000); // 休眠15秒以等待所有任務完成
Console.WriteLine("Main thread exiting.");
}
}
1.4.3 System.Threading.Tasks.Task
1.4.3.1 Task與ThreadPool區別
Task
和 ThreadPool
在 C#
中都是用於實現多執行緒和並行程式設計的機制,但它們在使用方式和效率上有所不同。
- 建立方式的區別
ThreadPool
:
ThreadPool
是一個執行緒池實現,它重用執行緒以減少建立和銷燬執行緒的開銷。
使用ThreadPool
時,通常透過呼叫ThreadPool.QueueUserWorkItem
方法將任務排隊到執行緒池中。
執行緒池中的執行緒由系統自動管理,開發者不需要手動建立和銷燬執行緒。Task
:
Task
是基於ThreadPool
的高層抽象,旨在簡化並行程式設計。
Task
提供了豐富的 API,可以方便地執行非同步操作,並等待其完成。
使用Task
時,通常透過Task.Run
方法或Task.Factory.StartNew
方法來建立並啟動任務。
Task 的實際執行依賴於任務排程器(TaskScheduler),預設情況下,任務排程器會使用執行緒池(ThreadPool)。
- 效率的比較
ThreadPool
:
適用於執行大量短暫任務的場景。
減少了執行緒建立和銷燬的開銷,因為執行緒會被重用來執行多個任務。
執行緒池會根據需求調整執行緒數量,但這個調整有一個上限,依賴於系統的最大執行緒數。Task
:
建立在ThreadPool
上,可以更好地利用ThreadPool
的執行緒池機制。
提供了更高階的抽象和 API,使得並行程式設計更加簡單和直觀。
支援非同步程式設計模式(async/await
),使得編寫非同步程式碼更加簡潔。
在處理大量並行任務時,Task 的效能通常優於直接操作 ThreadPool
1.4.3.2 Task 的使用方式
- 執行非同步操作:
使用Task.Run
方法將工作提交給執行緒池來非同步執行。
可以使用await
關鍵字等待任務完成並獲取結果。 - Async/Await:
在非同步方法中結合async
和await
關鍵字來編寫流暢的非同步程式碼。
非同步方法通常返回一個Task
或Task<T>
型別的物件。 - 建立和啟動 Task:
除了Task.Run
方法外,還可以直接例項化 Task 並透過.Start
方法手動啟動。
也可以使用TaskFactory
建立並開始新任務。 - 取消任務:
可以透過傳遞CancellationToken
到Task.Run
或Task.Factory.StartNew
來支援任務取消。 - 等待任務完成:
使用Task.Wait
方法可以同步等待任務完成(但通常不推薦,因為它可能導致當前執行緒阻塞)。
使用Task.WhenAll
等待多個任務同時完成。
使用Task.WhenAny
等待任意一個任務完成
1.4.3.3 Start 和 Wait
什麼時候需要呼叫 Start
呼叫
Start
是 手動啟動任務 的一種方式,僅適用於透過Task 建構函式
建立的任務。這類任務初始狀態是未啟動
的,必須顯式呼叫Start
方法將其加入執行緒池。
什麼時候不需要呼叫 Start?
透過
Task.Run
或Task.Factory.StartNew
建立的任務會自動開始執行
,無需呼叫Start
,而且嘗試呼叫Start
會導致異常。
這時候就需要Task.Wait
,阻塞呼叫執行緒,直到 Task 完成執行。如果任務已經完成,則立即返回。如果任務尚未開始或正在執行,則阻塞呼叫執行緒,直到任務完成。
區別與使用場景
方法 | 作用 | 適用場景 |
---|---|---|
Start | 顯式啟動任務 | 需要手動控制任務啟動時間,適用於透過建構函式建立的任務 |
Wait | 阻塞呼叫執行緒,等待任務完成 | 當需要確保任務完成後再繼續後續程式碼時使用 |
Start使用場景
建立方式 | 是否需要呼叫 Start | 推薦做法 |
---|---|---|
new Task(...) 建構函式 | 是 | 必須呼叫 Start 手動啟動任務 |
Task.Run | 否 | 不需要呼叫 Start,直接建立並執行 |
Task.Factory.StartNew | 否 | 不需要呼叫 Start,直接建立並執行 |
在現代 C# 開發中,通常建議使用 Task.Run
或 async/await
處理非同步任務,而不是透過顯式呼叫 Start 來管理任務
1.4.3.4 示例
基本任務建立與執行
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 建立一個簡單的任務,該任務會列印一條訊息
Task task = Task.Run(() =>
{
Console.WriteLine("Task is executing...");
});
// 等待任務完成
task.Wait();
Console.WriteLine("Task is completed.");
}
}
返回結果的任務
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 建立一個返回整數結果的任務
Task<int> task = Task.Run(() =>
{
// 模擬一些計算或處理
return 42;
});
// 等待並獲取任務結果
int result = task.Result;
Console.WriteLine("Task result: " + result);
}
}
並行執行多個任務
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 建立三個任務
Task task1 = Task.Run(() =>
{
Console.WriteLine("Task 1 is executing...");
});
Task<int> task2 = Task.Run(() =>
{
Console.WriteLine("Task 2 is executing...");
return 42;
});
Task task3 = Task.Run(() =>
{
Console.WriteLine("Task 3 is executing...");
});
// 等待所有任務完成
Task.WhenAll(task1, task2, task3).Wait();
Console.WriteLine("All tasks are completed.");
}
}
使用 ContinueWith 建立延續任務
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 建立一個任務
Task<int> task = Task.Run(() =>
{
// 模擬一些計算或處理
return 42;
});
// 建立一個在任務完成後執行的延續任務
task.ContinueWith(t =>
{
// 獲取任務結果並列印
int result = t.Result;
Console.WriteLine("Continuation task: Task result is " + result);
});
// 為了演示效果,這裡等待主任務完成(實際使用中可能不需要這樣做)
task.Wait();
}
}
1.4.4 async/await
1.4.4.1 解釋分析
在 C# 中,async
和 await
是用於非同步程式設計的關鍵字
async
:標記一個方法為非同步方法,使用時在方法返回型別前加上async
關鍵字
async
並不表示方法本身是非同步的,僅僅表示方法可以包含非同步
操作,並不會自動使方法非同步執行。方法中的程式碼仍然會按順序執行,直到遇到第一個await
await
:在非同步方法中等待另一個非同步操作完成。await
表示式會暫停當前方法的執行,直到被等待的任務完成,await
會暫停執行,但不阻塞執行緒,await
關鍵字會暫停當前方法的執行
。
await
會將方法的剩餘部分註冊為回撥
,以便在等待的操作完成後繼續執行,不會阻塞當前執行緒
在等待期間,執行緒可以執行其他任務,await
方法完成後將自動恢復執行。
注意
:await
必須放在async
方法內使用
1.4.4.2 非同步方法的異常處理
非同步方法中的異常會被包裝在 Task
物件中。要捕獲異常,可以使用 try-catch 語句,並在 await
表示式中捕獲,避免 async void
方法<除非是事件處理器,不推薦使用 async void,因為它不允許捕獲異常,且呼叫者無法等待其完成。應優先使用 Task
或 Task<T>
。
public async Task MyMethod()
{
try
{
await SomeAsyncMethod();
}
catch (Exception ex)
{
Console.WriteLine($"捕獲異常: {ex.Message}");
}
}
1.4.4.3 await 返回
await
只能用於 Task
或 Task<T>
,或實現了 GetAwaiter
方法的型別。
public class AsyncExample
{
public async Task<string> FetchDataAsync()
{
// 模擬一個耗時操作,例如網路請求
await Task.Delay(2000);
return "非同步資料";
}
public async Task RunExampleAsync()
{
try
{
string data = await FetchDataAsync();
Console.WriteLine(data);
}
catch (Exception ex)
{
Console.WriteLine($"捕獲異常: {ex.Message}");
}
}
}
1.4.4.4 示例分析
using System;
using System.Threading.Tasks;
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("開始獲取資料...");
// 呼叫非同步方法,並等待其結果
string result = await GetDataAsync();
Console.WriteLine($"資料獲取完成: {result}");
}
// 定義一個非同步方法,用 async 標記
public static async Task<string> GetDataAsync()
{
// 使用 Task.Delay 來模擬一個耗時操作,例如網路請求
await Task.Delay(3000); // 等待3秒
return "這裡是獲取到的資料";
}
}
結果
開始獲取資料...
(等待 3 秒)
資料獲取完成: 這裡是獲取到的資料
詳細解釋:
async
關鍵字:GetDataAsync
方法被標記為async
,表示它是一個非同步方法,可以使用await
關鍵字來等待非同步任務完成。await
關鍵字:在GetDataAsync
中,使用 await Task.Delay(3000) 來模擬一個耗時的操作。await
關鍵字會暫停方法的執行,直到 Task.Delay 完成。- 返回型別:
async
方法的返回型別通常是Task
或Task<T>
(泛型形式)。在本例中,GetDataAsync 返回Task<string>
,表示返回的是一個包含 string 結果的任務。
1.4.5 System.Threading.Tasks.Parallel
System.Threading.Tasks.Parallel
類並行執行,不是真正的多執行緒,但用於並行處理資料
這個類是 C# 中用於並行程式設計
的一個重要工具,並行地執行迴圈或操作,從而充分利用多核處理器的效能。
Parallel
類利用執行緒池(ThreadPool
)來管理執行緒,並自動將任務分配給可用的執行緒。這意味著開發者不需要顯式地建立和管理執行緒,而是將精力集中在任務的定義和並行執行上。儘管Parallel
類並不直接建立執行緒,但它確實利用了多執行緒來實現並行處理。執行緒池中的執行緒是實際執行任務的實體,而Parallel
類則提供了更高層次的抽象,使開發者能夠更容易地實現並行處理。
因此,可以說Parallel
類提供了一種並行處理資料的方式,這種方式基於多執行緒,但又不完全等同於多執行緒程式設計。它更多地是一種任務並行化的程式設計模型,允許開發者以更簡單、更直觀的方式實現並行處理。
Parallel
類是一個靜態類,它提供了幾個靜態方法用於並行執行程式碼,主要包括 Parallel.For
、Parallel.ForEach
和 Parallel.Invoke
:
Parallel.For
:用於並行執行 for 迴圈,它會自動將迴圈的迭代分配給多個執行緒,並不直接提供控制迭代間隔的功能
csharp
Parallel.For(0, 10, i =>
{
Console.WriteLine($"Iteration {i} is running on thread {Thread.CurrentThread.ManagedThreadId}");
});
Parallel.ForEach
:用於並行執行foreach
迴圈。它會自動將集合中的元素分配給多個執行緒。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
Parallel.ForEach(numbers, number =>
{
Console.WriteLine($"Processing {number} on thread {Thread.CurrentThread.ManagedThreadId}");
});
Parallel.Invoke
:行執行多個操作。你可以傳遞多個要並行執行的 Action 或 Func 委託。
Parallel.Invoke(
() => { /* Task 1 */ },
() => { /* Task 2 */ },
() => { /* Task 3 */ }
);