此入門教程是記錄下方參考資料視頻的過程
開發工具:Visual Studio 2019
目錄
C# 異步編程基礎(六)Continuation 繼續/延續 、TaskCompletionSource、實現 Task.Delay
C# 異步編程基礎(十) 取消(cancellation)、進度報告、TAP(Task-Based Asynchronous Pattern)、Task組合器
線程
- 線程是一個可執行路徑,它可以獨立於其它線程執行
- 每一個線程都在操作系統的進程(Process)內支線,而操作系統進程提供了程序運行的獨立環境
- 單線程應用,在進程的獨立環境里只跑一個線程,所以該線程擁有獨占權
- 多線程應用,單個進程中會跑多個線程,它們會共享當前的執行環境(尤其是內存)
例如,一個線程在后台讀取數據,另一個線程在數據到達后進行展示
這個數據就被稱作是共享的狀態 - 例子:
- 在單核計算機上,操作系統必須為每個線程分配“時間片”(在Windows中通常為20毫秒)來模擬並發,從而導致重復的x和y塊
- 在多核或多處理器計算機上,這兩個線程可以真正地並行執行(可能受到計算機上其它活動進程的競爭)
- 在本例中,由於控制台處理並發請求的機制的微妙性,您仍然會得到重復的x和y塊
public static void Main()
{
//開辟了一個新的線程 Thread
Thread t=new Thread(WriteY);
t.Start();//運行WirteY
Console.WriteLine("Thread t has ended!");
//同時在主線程也做一些工作
for(int i=0;i<10;i++)
{
Console.Write("x");
}
}
public static void WriteY()
{
for(int i=0;i<1000;i++)
{
Console.Write("y");
}
}
- 術語:線程被搶占
線程在這個時候就可以被稱為搶占了:它的執行與另外一個線程上代碼的執行交織的那一點
線程的一些屬性
- 線程一旦開始執行,IsAlive就是true,線程結束就變成false
- 線程結束的條件就是:線程構造函數傳入的委托結束了執行
- 線程一旦結束,就無法再重啟
- 每個線程都有個Name屬性,通常用於調試
線程Name只能設置一次,以后更改就會拋出異常 - 靜態的Thread.CurrentThread屬性,會返回當前執行的線程
例子
public static void Main()
{
//開辟了一個新的線程 Thread
Thread t=new Thread(WriteY);
t.Name="Y Thread ...";
t.Start();//運行WirteY
Console.WriteLine("Thread.CurrentThread.Name");
//同時在主線程也做一些工作
for(int i=0;i<10;i++)
{
Console.Write("x");
}
}
public static void WriteY()
{
Console.WriteLine(Thread.CurrentThread.Name);
for(int i=0;i<1000;i++)
{
Console.Write("y");
}
}
結果
Join and Sleep
- 調用Join方法,就可以等待另一個線程結束
可以理解為“等待該線程終止”,也就是在子線程調用了Join()方法后面的代碼,只有等到子線程結束了才能執行。
例子
public static void Main()
{
Thread t=new Thread(Go);
t.Start();
t.Join();
Console.WriteLine("Thread t has ended!");
}
public static void Go()
{
for(int i=0;i<1000;i++)
{
Console.Write("y");
}
}
結果
- 添加超時:
調用Join的時候,可以設置一個超時,用毫秒或者TimeSpan都可以
如果返回true,那就是線程結束了
如果超時了(在限制時間內未完成),就返回false
例子
static Thread thread1, thread2;
public static void Main()
{
thread1 = new Thread(ThreadProc);
thread1.Name = "Thread1";
thread1.Start();
thread2 = new Thread(ThreadProc);
thread2.Name = "Thread2";
thread2.Start();
}
private static void ThreadProc()
{
Console.WriteLine("\nCurrent thread:{0}", Thread.CurrentThread.Name);
if (Thread.CurrentThread.Name == "Thread1" && thread2.ThreadState != ThreadState.Unstarted)
{
if (thread2.Join(2000))
{
Console.WriteLine("Thread2 has termminated.");
}
else
{
Console.WriteLine("The timeout has elapsed and Thread1 will resume.");
}
}
Thread.Sleep(4000);
Console.WriteLine("\nCurrent thread:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Thread1:{0}", thread1.ThreadState);
Console.WriteLine("Thread2:{0}", thread2.ThreadState);
}
結果
- Thread.Sleep()方法會暫停當前的線程,並等一段時間,參數可以時毫秒,也可以是TimeSpan
例子
public static void Main()
{
for(int i=0;i<5;i++)
{
Console.WriteLine("Sleep for 2 seconds");
Thread.Sleep(2000);
}
Console.WriteLine("Main thread exits");
}
結果就是每過 2000 毫秒輸出一個 Sleep for 2 seconds ,最后輸出 Main thread exits
- 注意:
- Thread.Sleep(0)這樣調用會導致線程立即放棄當前的時間片,自動將CPU移交給其它線程
- Thread.Yield()做同樣的事情,但是它只會把執行交給同一處理器上的其它線程
- 當等待Sleep或Join的時候,線程處於阻塞的狀態
- Sleep(0)或Yield有時在高級性能調試的生產代碼中很有用。它也是一個很好的診斷工具,有助於發現線程安全問題:
如果在代碼中的任何地方插入Threa.Yield()就破壞了程序,那么你的程序幾乎肯定有bug
阻塞 Blocking
- 如果線程的執行由於某種原因被導致暫定,那么就認定該線程被阻塞了
例如在Sleep()或者通過Join()等待其它線程的結束 - 被阻塞的線程會立即將其處理器的時間片生成給其它線程,從此不會再消耗處理器時間,直到滿足其阻塞條件為止
可以通過ThreadState這個屬性來判斷線程是否處於被阻塞的狀態
bool blocked=(someThread.ThreadState & ThreadState.WaitSleepJoin)!=0;
必須這樣寫
ThreadState
- ThreadState是一個flags enum,通過按位的形式,可以合並數據的選項
- 但是它大部分的枚舉值都沒什么用,下面的代碼將ThreadState剝離為四個最有用的值之一:Unstarted、Running、WaitSleepJoin和Stopped
public static ThreadState SimpleThreadState(ThreadState ts)
{
return ts&(ThreadState.Unstarted|ThreadState.WaitSleepJoin|ThreadState.Stopped);
}
- ThreadState屬性可用於診斷的目的,但不適用於同步,因為線程狀態可能會在測試ThreadState和對該信息進行操作之間發生變化
解除阻塞 Unblocking
- 當遇到下列四種情況的時候,就會解除阻塞:
阻塞條件被滿足
操作超時(如果設置超時的話)
通過Thread。Interrupt()進行打斷
通過Thread.Abort()進行中止
上下文切換
- 當線程阻塞或解除阻塞時,操作系統將執行上下文切換。這會產生少量開銷,通常為1或2微秒
I/O-bound vs Compute-bound(或CPU-bound)
- 一個花費大部分時間等待某事發生的操作稱為I/O-bound(什么事都不干)
I/O綁定操作通常涉及輸入或輸出,但這不是硬性要求:Thread.Sleep()也被視為I/O-bound - 相反,一個花費大部分時間執行CPU密集型工作的操作稱為Compute-bound(一直在運行)
阻塞 vs 忙等待(自旋) Blocking vs Spinning
- IO-bound操作的工作方式有兩種
在當前線程上的同步等待
Console.ReadLine(),Thread.Sleep(),Thread.Join()...
異步的操作,在稍后操作完成時觸發一個回調動作 - 同步等待的I/O-bound操作將大部分時間花在阻塞線程上
- 它們也可以周期性的在一個循環里進行“打轉(自旋)”
//不徹底的忙等待(自旋)
while(DateTime.Now < nextStartTime)
{
Thread.Sleep(100);
}
//徹底的忙等待(自旋),CPU一直在工作
while(DateTime.Now < nextStartTime);
- 在忙等待和阻塞方面有有一些細微差別
首先,如果您希望條件很快得到滿足(可能在幾微秒之內),則短暫自旋可能會很有效,因為它避免了上下文切換的開銷和延遲
.NET Framework提供了特殊的方法和類來提供幫助SpinLock和SpinWait
其次,阻塞也不是零成本。這是因為每個線程在生存期間會占用大約 1 MB的內存,並會給CLR和操作系統帶來持續的管理開銷
因此,在需要處理成百上千個並發操作的大量I/O-bound程序的上下文中,阻塞可能會很麻煩
所以,此類程序需要使用基於回調的方法,在等待時完全撤銷其線程