C# sync/async 同步/異步(附:進程-線程-多線程--的關系)


------------------------------------進程-線程-多線程------------------------------------------------------------------------------------------------------------------------------------

    /// 多線程是.Net開發非常重要的一塊兒,
    /// 但是很多開發者工作多年,對多線程幾乎不用/很畏懼/不明所以,寫代碼的時候沒有考慮多線程的場景
    /// 
    /// 
    /// 進程:計算機概念,程序在服務器運行時占據全部計算資源綜總和
    ///       虛擬的,
    /// 線程:計算機概念,進程在響應操作時最小單位,也包含CPU 內存  網絡  硬盤IO
    ///       虛擬的概念,更加看不見摸不着
    /// 一個進程會包含多個線程;線程隸屬於某個進程,進程銷毀線程也就沒了
    /// 句柄:其實是個long數字,是操作系統標識應用程序
    /// 多線程:計算機概念,一個進程有多個線程同時運行
    /// 
    /// C#里面的多線程:
    /// Thread類是C#語言對線程對象的一個封裝
    /// 
    /// 為什么可以多線程呢?
    /// 1 多個CPU的核可以並行工作,
    ///   4核8線程,這里的線程指的是模擬核
    ///   
    /// 2 CPU分片,1s的處理能力分成1000份,操作系統調度着去響應不同的任務
    ///   從宏觀角度來說,感覺就是多個任務在並發執行
    ///   從微觀角度來說,一個物理cpu同一時刻只能為一個任務服務
    /// 
    /// 並行:多核之間叫並行
    /// 並發:CPU分片的並發
    ///   
    /// 同步異步:
    ///       同步方法:發起調用,完成后才繼續下一行;非常符合開發思維,有序執行;
    ///                 誠心誠意的請人吃飯,邀請Nick,Nick要忙一會兒,等着Nick完成后,再一起去吃飯
    ///       異步方法:發起調用,不等待完成,直接進入下一行,啟動一個新線程來完成方法的計算
    ///                 客氣一下的請人吃飯,邀請亡五,亡五要忙一會兒,你忙着我去吃飯了,你忙完自己去吃飯吧

什么是進程?

當一個程序開始運行時,它就是一個進程,進程包括運行中的程序和程序所使用到的內存和系統資源。

而一個進程又是由多個線程所組成的。

什么是線程?

線程是程序中的一個執行流,每個線程都有自己的專有寄存器(棧指針、程序計數器等),但代碼區是共享的,即不同的線程可以執行同樣的函數。

線程和進程的關系?

一個進程會包含多個線程:線程是隸屬於某個進程,進程銷毀,線程則消失

什么是多線程?

多線程是指程序中包含多個執行流,即在一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個並行執行的線程來完成各自的任務。
多線程的好處: 

可以提高CPU的利用率。在多線程程序中,一個線程必須等待的時候,CPU可以運行其它的線程而不是等待,這樣就大大提高了程序的效率。

多線程的不利方面:

線程也是程序,所以線程需要占用內存,線程越多占用內存也越多; 

多線程需要協調和管理,所以需要CPU時間跟蹤線程; 

線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題;

線程太多會導致控制太復雜,最終可能造成很多Bug;

  何時使用多線程

    多線程程序一般被用來在后台執行耗時的任務。主線程保持運行,並且工作線程做它的后台工作。對於Windows Forms程序來說,如果主線程試圖執行冗長的操作,鍵盤和鼠標的操作會變的遲鈍,程序也會失去響應。由於這個原因,應該在工作線程中運行一個耗時任務時添加一個工作線程,即使在主線程上有一個有好的提示“處理中...”,以防止工作無法繼續。這就避免了程序出現由操作系統提示的“沒有相應”,來誘使用戶強制結束程序的進程而導致錯誤。模式對話框還允許實現“取消”功能,允許繼續接收事件,而實際的任務已被工作線程完成。BackgroundWorker恰好可以輔助完成這一功能。

   在沒有用戶界面的程序里,比如說Windows Service, 多線程在當一個任務有潛在的耗時,因為它在等待另台電腦的響應(比如一個應用服務器,數據庫服務器,或者一個客戶端)的實現特別有意義。用工作線程完成任務意味着主線程可以立即做其它的事情。

   另一個多線程的用途是在方法中完成一個復雜的計算工作。這個方法會在多核的電腦上運行的更快,如果工作量被多個線程分開的話(使用Environment.ProcessorCount屬性來偵測處理芯片的數量)。

   一個C#程序稱為多線程的可以通過2種方式:明確地創建和運行多線程,或者使用.NET framework的暗中使用了多線程的特性——比如BackgroundWorker類, 線程池threading timer,遠程服務器,或Web Services或ASP.NET程序。在后面的情況,人們別無選擇,必須使用多線程;一個單線程的ASP.NET web server不是太酷,即使有這樣的事情;幸運的是,應用服務器中多線程是相當普遍的;唯一值得關心的是提供適當鎖機制的靜態變量問題。

  何時不要使用多線程

    多線程也同樣會帶來缺點,最大的問題是它使程序變的過於復雜,擁有多線程本身並不復雜,復雜是的線程的交互作用,這帶來了無論是否交互是否是有意的,都會帶來較長的開發周期,以及帶來間歇性和非重復性的bugs。因此,要么多線程的交互設計簡單一些,要么就根本不使用多線程。除非你有強烈的重寫和調試欲望。

當用戶頻繁地分配和切換線程時,多線程會帶來增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,當只有一個或兩個工作線程要比有眾多的線程在相同時間執行任務快的多。

------------------------------------C# sync/async 同步/異步------------------------------------------------------------------------------------------------------------------------------------

一:同步方法:

在程序繼續執行之前需要等待同步方法執行完畢返回結果

通俗的例子就是: 邀請wss次飯,wss要忙一會兒,邀請人等着wss完成后,再一起去吃飯,這就是所謂的誠心誠意的請人吃飯。下面我通過代碼來舉例來說明一下同步方法:

/// <summary>
/// 一個比較耗時耗資源的私有方法
/// </summary>
/// <param name="name"></param>
private void DoSomethingLong(string name)
{
    Console.WriteLine($"******DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}*******");
    long lResult = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        lResult += i;
    }
    Console.WriteLine($"*****DoSomethingLong   End  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}*********");
}

/// <summary>
/// 同步方法
/// </summary>
private void syncWay()
{
    Console.WriteLine($"******同步方法開始執行,執行線程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,執行開始時間:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】*****");
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"sysnc_{i}");
        this.DoSomethingLong(name);
    }
    Console.WriteLine($"****同步方法執行結束,執行線程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,執行開始時間:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】******");
}

直接調用執行 syncWay(); 則會輸出如下圖:

 

我們可以通過上圖曉得:DoSomethingLong是一步一步執行的,而且同步方法的線程Id都是一致的,也可以看出執行過程的耗時也蠻長。

二:異步方法:

在被調用之后立即返回以便程序在被調用方法完成其任務的同時執行其它操作

通俗的例子就是: 邀請wss次飯,wss要忙一會兒,然后wss自己先忙着吧,邀請人先去吃飯了,wss忙完自己去吃飯吧,這就是所謂的客氣一下的請人吃飯。下面我接着使用實例來認識異步方法:

/// <summary>
/// 一個比較耗時耗資源的私有方法
/// </summary>
/// <param name="name"></param>
private void DoSomethingLong(string name)
{
    Console.WriteLine($"*****DoSomethingLong開始;參數【{name}】;線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;當前時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***");
    long result = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        result += i;
    }
    Console.WriteLine($"*****DoSomethingLong結束;參數【{name}】;線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】;當前時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")};result:{result}***");
}

private void AsyncWay()
{
    Console.WriteLine($"******異步方法開始執行,執行線程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,執行開始時間:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】*****");
    Action<string> action = this.DoSomethingLong;
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"async_{i}");                
        action.BeginInvoke(name, null, null);//委托自身需要的參數 + 2個異步參數(這兩個參數我們在下面詳細說明)
    }
    Console.WriteLine($"****異步方法執行結束,執行線程ID:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】,執行開始時間:【 {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}】******");
}

然后通過執行AsyncWay()這個方法,會得到如下截圖:

 我們可以通過上面的圖得到如下幾點:

1:不卡主線程改善用戶體驗。web或者winform如果處理耗時的任務,異步方法會使頁面或者winform不卡界面,這主要是主線程(線程01)閑置,計算任務交給子線程(線程03,04,05,06)完成。這個可以改善用戶體驗。一般在web應用中異步方法會做發短信/記日志等功能。

2:異步方法耗時短,這個是最明顯的。通過我們觀察cpu的曲線圖,會總結出:多線程其實是資源換性能。但是也不能亂用多線程,主要是

  • A: 資源不是無限的 
  • B: 資源調度損耗(這個電腦配置有關系)

3:異步方法是無序的,主要體現在如下:

  • A:啟動無序,因為線程資源是向操作系統申請的,由操作系統的調度策略決定,所以啟動順序隨機
  • B:同一個任務同一個線程,執行時間也不確定,CPU分片
  • C:由上面兩條也得到結束也無序

.NET Framework 允許異步調用任何方法。定義與您需要調用的方法具有相同簽名的委托;公共語言運行庫將自動為該委托定義具有適當簽名的 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法用於啟動異步調用。它與您需要異步執行的方法具有相同的參數,只不過還有兩個額外的參數

      • 第一個參數表示一個委托,需要傳遞一個方法,當該線程結束時會調用這個方法
      • 第二個參數可以設置為任意對象,以便在回調函數中訪問它,在這里所用的是上面定義的委托a。

三:異步方法如何控制順序?

就是說我們想要在一個異步方法全部執行完成后再執行某一部分內容,可以通過以下幾種方式來實現:

1:BeginInvoke

立即返回,不等待異步調用完成。

使用BeginInvoke的AsyncCallback,將后續動作通過回調參數AsyncCallback傳遞進去,子線程完成計算后,去調用這個回調委托,BeginInvoke這個我們上面有提到,下面還是以代碼來解釋說明。

private void FirstWay()
{
    Console.WriteLine($"**開始線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");

    Action<string> action = this.DoSomethingLong;
    IAsyncResult asyncResult = null;//是對異步調用操作的描述
  
    //回調委托方法:將后續動作通過回調參數傳遞進去,子線程完成計算后,去調用這個回調委托
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"asyncResult和ar是否是同一個對象:{object.ReferenceEquals(ar, asyncResult)}"); //可以說明ar就是asyncResult
        Console.WriteLine($"dosomething計算成功了。ar.AsyncState=【{ar.AsyncState}】。線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】");
    };
    //第1個參數:是action需要的參數;
    //第2個參數:AsyncCallback,就是執行完action方法后要執行的內容;
    //第3個參數:是一個object的對象,指的是IAsyncResult函數中的AsyncState,可以作為參數等傳進去
    asyncResult = action.BeginInvoke("異步控制順序", callback, "ar.AsyncState參數");
    Console.WriteLine($"**結束:線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
}

執行FirstWay()會得到如下結果:

我們通過上圖可以分析出:

  • A:BeginInvoke是一個異步方法,因為主線程【01】只負責打印,而DoSomethingLong這個方法是開啟一個新的線程【03】執行
  • B:AsyncCallback這個委托的參數其實是 action.BeginInvoke返回的結果類型為IAsyncResult,就是例子中的asyncResult
  • C:action.BeginInvoke第三個參數就是IAsyncResult中的AsyncState,是一個object類型,可以作為參數傳入到AsyncCallback函數中
  • D:action.BeginInvoke這個函數的執行順序是:先執行Action函數,然后執行完返回一個IAsyncResult結果asyncResult,然后把asyncResult作為參數傳入到AsyncCallback這個委托中,最后再執行AsyncCallback委托

2:BeginInvoke 返回 IasyncResult,屬性IAsyncResult.IsCompleted

可用於監視調用進度。

private void SecondWay()
 {
     Console.WriteLine($"**開始線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
     Action<string> action = this.DoSomethingLong;
     IAsyncResult asyncResult = action.BeginInvoke("異步控制順序", null, null);

     //通過IsComplate等待,卡界面--主線程在等待,邊等待邊提示
     int i = 0;
     while (!asyncResult.IsCompleted)
     {
         if (i < 9)
         {
             Console.WriteLine($"DoSomethingLong完成{++i * 10}%....");
         }
         else
         {
             Console.WriteLine($"DoSomethingLong完成99.999999%....");
         }
         Thread.Sleep(300);
     }
     Console.WriteLine("DoSomethingLong完全執行結束!");
     Console.WriteLine($"**結束:線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
 }

執行SecondWay()會返回如下:

 

3:使用 WaitOne等待,

即時等待  限時等待,示例代碼如下:

private void ThirdWay()
 {
     Console.WriteLine($"**開始線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
     Action<string> action = this.DoSomethingLong;
     IAsyncResult asyncResult = action.BeginInvoke("第三種異步控制順序", null, null);
     asyncResult.AsyncWaitHandle.WaitOne();//直接等待任務完成
     //asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待任務完成
     //asyncResult.AsyncWaitHandle.WaitOne(1000);//最多等待1000ms,超時就不等了

     Console.WriteLine("DoSomethingLong完全執行結束!");
     Console.WriteLine($"**結束:線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
 }

執行 ThirdWay()得到以下結果:

 

 

4:使用EndInvoke :

即時等待,而且可以獲取委托的返回值 一個異步操作只能End一次,示例代碼如下:

EndInvoke 方法用於檢索異步調用結果。調用 BeginInvoke 后可隨時調用 EndInvoke 方法;如果異步調用未完成,EndInvoke 將一直阻塞到異步調用完成。
EndInvoke 的參數包括您需要異步執行的方法的 out 和 ref 參數(在 Visual Basic 中為 <Out> ByRef 和 ByRef)以及由BeginInvoke 返回的 IAsyncResult。

private void ForthWay()
{
    Console.WriteLine($"**開始線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");

    Action<string> action = this.DoSomethingLong;
    IAsyncResult asyncResult4Action = action.BeginInvoke("第四種EndInvoke異步控制順序", null, null);
    action.EndInvoke(asyncResult4Action);


    Func<int> func = () => { return 10; };
    IAsyncResult asyncResult4Fun = func.BeginInvoke(ar => 
    {
        Console.WriteLine($"IAsyncResult中的線程Id:【{ Thread.CurrentThread.ManagedThreadId.ToString("00")}】");
    }, null);
    int result=func.EndInvoke(asyncResult4Fun);//里面的參數不能為空

    Console.WriteLine($"endInvoke返回的func委托的值:{result}");
    Console.WriteLine("DoSomethingLong完全執行結束!");
    Console.WriteLine($"**結束:線程Id:【{Thread.CurrentThread.ManagedThreadId.ToString("00")}】時間:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}**");
}

執行ForthWay()會得到如下:

上面的代碼有Action和Fun兩個方法,然后執行有時候會出現上圖的結果,IAsyncResult最后一個輸出來,通過這個我們可以得知: EndInvoke是讓主線程的動作,等着子線程完成,但是這個等待並不會等待回調函數。所以會出現上面的結果

說明:以上四種方法中DoSomethingLong這個函數我們都使用上面異步或者同步方法的示例

 通過以上4種方法總結得出,使用 BeginInvoke 和 EndInvoke 進行異步調用的常用方法。調用了 BeginInvoke 后,可以:

  1. 進行某些操作,然后調用 EndInvoke 一直阻塞到調用完成。
  2. 使用 IAsyncResult.AsyncWaitHandle 獲取 WaitHandle,使用它的 WaitOne 方法將執行一直阻塞到發出 WaitHandle 信號,然后調用EndInvoke。這里主要是主程序等待異步方法,等待異步方法的結果。
  3. 輪詢由 BeginInvoke 返回的 IAsyncResult,IAsyncResult.IsCompeted確定異步調用何時完成,然后調用 EndInvoke。此處理個人認為與相同。
  4. 將用於回調方法的委托傳遞給 BeginInvoke。該方法在異步調用完成后在 ThreadPool 線程上執行,它可以調用 EndInvoke。這是在強制裝換回調函數里面IAsyncResult.AsyncState(BeginInvoke方法的最后一個參數)成委托,然后用委托執行EndInvoke。
  5. 警告 始終在異步調用完成后調用 EndInvoke。

微軟文檔:

IAsyncResult:https://docs.microsoft.com/zh-cn/dotnet/api/system.iasyncresult?view=netframework-4.8

AsyncCallback:https://docs.microsoft.com/zh-cn/dotnet/api/system.asynccallback?view=netframework-4.8

感謝軟謀教育Eleven老師!!


免責聲明!

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



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