C# 異步編程基礎(一)線程和阻塞


此入門教程是記錄下方參考資料視頻的過程
開發工具:Visual Studio 2019

參考資料:https://www.bilibili.com/video/BV1Zf4y117fs

目錄

C# 異步編程基礎(一)線程和阻塞

C# 異步編程基礎(二)線程安全、向線程傳遞數據和異常處理

C# 異步編程基礎(三)線程優先級、信號和線程池

C# 異步編程基礎(四) 富客戶端應用程序的線程 和 同步上下文 Synchronization Contexts

C# 異步編程基礎(五)Task

C# 異步編程基礎(六)Continuation 繼續/延續 、TaskCompletionSource、實現 Task.Delay

C# 異步編程基礎(七)異步原理

C# 異步編程基礎(八) 異步函數

C# 異步編程基礎(九) 異步中的同步上下文、ValueTask

C# 異步編程基礎(十) 取消(cancellation)、進度報告、TAP(Task-Based Asynchronous Pattern)、Task組合器

線程

  1. 線程是一個可執行路徑,它可以獨立於其它線程執行
  2. 每一個線程都在操作系統的進程(Process)內支線,而操作系統進程提供了程序運行的獨立環境
  3. 單線程應用,在進程的獨立環境里只跑一個線程,所以該線程擁有獨占權
  4. 多線程應用,單個進程中會跑多個線程,它們會共享當前的執行環境(尤其是內存)
    例如,一個線程在后台讀取數據,另一個線程在數據到達后進行展示
    這個數據就被稱作是共享的狀態
  5. 例子:
    1. 在單核計算機上,操作系統必須為每個線程分配“時間片”(在Windows中通常為20毫秒)來模擬並發,從而導致重復的x和y塊
    2. 在多核或多處理器計算機上,這兩個線程可以真正地並行執行(可能受到計算機上其它活動進程的競爭)
    3. 在本例中,由於控制台處理並發請求的機制的微妙性,您仍然會得到重復的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");
    }
}
  1. 術語:線程被搶占
    線程在這個時候就可以被稱為搶占了:它的執行與另外一個線程上代碼的執行交織的那一點

線程的一些屬性

  1. 線程一旦開始執行,IsAlive就是true,線程結束就變成false
  2. 線程結束的條件就是:線程構造函數傳入的委托結束了執行
  3. 線程一旦結束,就無法再重啟
  4. 每個線程都有個Name屬性,通常用於調試
    線程Name只能設置一次,以后更改就會拋出異常
  5. 靜態的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

  1. 調用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");
    }
}

結果

  1. 添加超時:
    調用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);
}

結果

  1. 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

  1. 注意:
    1. Thread.Sleep(0)這樣調用會導致線程立即放棄當前的時間片,自動將CPU移交給其它線程
    2. Thread.Yield()做同樣的事情,但是它只會把執行交給同一處理器上的其它線程
    3. 當等待Sleep或Join的時候,線程處於阻塞的狀態
    4. Sleep(0)或Yield有時在高級性能調試的生產代碼中很有用。它也是一個很好的診斷工具,有助於發現線程安全問題:
      如果在代碼中的任何地方插入Threa.Yield()就破壞了程序,那么你的程序幾乎肯定有bug

阻塞 Blocking

  1. 如果線程的執行由於某種原因被導致暫定,那么就認定該線程被阻塞了
    例如在Sleep()或者通過Join()等待其它線程的結束
  2. 被阻塞的線程會立即將其處理器的時間片生成給其它線程,從此不會再消耗處理器時間,直到滿足其阻塞條件為止
    可以通過ThreadState這個屬性來判斷線程是否處於被阻塞的狀態
    bool blocked=(someThread.ThreadState & ThreadState.WaitSleepJoin)!=0;
    必須這樣寫

ThreadState

  1. ThreadState是一個flags enum,通過按位的形式,可以合並數據的選項
  2. 但是它大部分的枚舉值都沒什么用,下面的代碼將ThreadState剝離為四個最有用的值之一:Unstarted、Running、WaitSleepJoin和Stopped
public static ThreadState SimpleThreadState(ThreadState ts)
{
    return ts&(ThreadState.Unstarted|ThreadState.WaitSleepJoin|ThreadState.Stopped);
}
  1. ThreadState屬性可用於診斷的目的,但不適用於同步,因為線程狀態可能會在測試ThreadState和對該信息進行操作之間發生變化

解除阻塞 Unblocking

  1. 當遇到下列四種情況的時候,就會解除阻塞:
    阻塞條件被滿足
    操作超時(如果設置超時的話)
    通過Thread。Interrupt()進行打斷
    通過Thread.Abort()進行中止

上下文切換

  1. 當線程阻塞或解除阻塞時,操作系統將執行上下文切換。這會產生少量開銷,通常為1或2微秒

I/O-bound vs Compute-bound(或CPU-bound)

  1. 一個花費大部分時間等待某事發生的操作稱為I/O-bound(什么事都不干)
    I/O綁定操作通常涉及輸入或輸出,但這不是硬性要求:Thread.Sleep()也被視為I/O-bound
  2. 相反,一個花費大部分時間執行CPU密集型工作的操作稱為Compute-bound(一直在運行)

阻塞 vs 忙等待(自旋) Blocking vs Spinning

  1. IO-bound操作的工作方式有兩種
    在當前線程上的同步等待
      Console.ReadLine(),Thread.Sleep(),Thread.Join()...
    異步的操作,在稍后操作完成時觸發一個回調動作
  2. 同步等待的I/O-bound操作將大部分時間花在阻塞線程上
  3. 它們也可以周期性的在一個循環里進行“打轉(自旋)”
//不徹底的忙等待(自旋)
while(DateTime.Now < nextStartTime)
{
      Thread.Sleep(100);
}
//徹底的忙等待(自旋),CPU一直在工作
while(DateTime.Now < nextStartTime);
  1. 在忙等待和阻塞方面有有一些細微差別
    首先,如果您希望條件很快得到滿足(可能在幾微秒之內),則短暫自旋可能會很有效,因為它避免了上下文切換的開銷和延遲
      .NET Framework提供了特殊的方法和類來提供幫助SpinLock和SpinWait
    其次,阻塞也不是零成本。這是因為每個線程在生存期間會占用大約 1 MB的內存,並會給CLR和操作系統帶來持續的管理開銷
      因此,在需要處理成百上千個並發操作的大量I/O-bound程序的上下文中,阻塞可能會很麻煩
      所以,此類程序需要使用基於回調的方法,在等待時完全撤銷其線程

線程和阻塞 結束


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM