歡迎光臨
每天分享高質量文章

C#並行程式設計(5):需要知道的非同步

非同步與並行的聯絡

大家知道“並行”是利用CPU的多個核心或者多個CPU同時執行不同的任務,我們不關心這些任務之間的依賴關係。
但是在我們實際的業務中,很多工之間是相互影響的,比如統計車間全年產量的運算要依賴於各月產量的統計結果。假如你想在計算月產量的時候做些其他事情,如匯出生產異常報表,“非同步”就可以登上舞臺了。

說到非同步,必須要先提一下同步。一圖勝千言:

圖中操作C的執行依賴B的結果,B的執行依賴A的結果。執行緒1連續執行操作A、B、C便是一個同步過程;相對地,執行緒1執行完A後把結果給執行緒2,執行緒2開始執行B,完成後把B的結果通知到執行緒1,執行緒1開始執行C,執行緒1在等待操作B結果的時候執行了D,這就是一個非同步的過程;此外,非同步過程中,B和D是並行執行的。

並行會提高業務的執行效率,但非同步不會,非同步甚至會拖慢業務的執行,比如上面A->B->C的執行過程。非同步是讓等待變得更有價值,這種價值則體現在多個業務的並行上

C#中的非同步

在需要長時間等待的地方都可以使用非同步,比如讀寫檔案、訪問網路或者處理圖片。特別是在UI執行緒中,我們要保持介面的響應性,耗時的操作最好都使用非同步的方式執行。

.NET提供了三種非同步樣式:

  • IAsyncResult樣式(APM)
  • 基於事件的非同步樣式(EAP)
  • 基於任務的非同步樣式(TAP)

其中基於任務的非同步樣式是.NET推薦的非同步程式設計方式。

IAsyncResult非同步樣式APM

下麵是IAsyncResult基於委託的用法。





private delegate void AsyncWorkCaller(int workNo);
            
public static void Run()
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work.");
    AsyncWorkCaller caller = DoWork;
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did the callback. [{ar.AsyncState}]");
    };
    IAsyncResult result = caller.BeginInvoke(1, callback, "callback msg");
    DoWork(2);
    
    caller.EndInvoke(result);
    DoWork(3);
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work.");
}





private static void DoWork(int workNo)
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}.");
    Thread.Sleep(1000);
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}.");
}

我們使用BeginInvoke來非同步執行作業1,同時可以執行作業2,呼叫EndInvoke的時候,當前執行緒被阻塞直到作業1完成。我們也可以使用result.AsyncWaitHandle.WaitOne()來等待非同步作業完成,同樣會阻塞當前執行緒。此外,可以為非同步作業增加回呼,非同步作業在完成時會執行回呼函式。

基於事件的非同步樣式EAP

事件大家不會陌生,我們在Winform程式設計的時候,總會用到事件。下麵是利用BackgroundWorker實現的一個基於事件的簡單非同步過程。我們給非同步物件(這裡是BackgroundWorker)訂閱DoWorkRunWorkCompleted事件,當呼叫RunWorkerAsync時,觸發非同步物件的工作事件,此時會開闢一個新執行緒來執行標的操作。標的操作完成時,觸發工作完成事件,執行後續操作。與IAsyncResult樣式不同的是,作業完成後的後續操作會在另外的一個執行緒執行,而IAsyncResult樣式中,完成回呼會在標的操作的執行執行緒中執行。

public static class EventBasedAsync
{
    private static readonly BackgroundWorker worker = new BackgroundWorker();

    static EventBasedAsync()
    {
        worker.DoWork += Worker_DoWork;
        worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
    }

    public static void Run()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work.");
        worker.RunWorkerAsync(1);
        DoWork(2);
        DoWork(3);
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work.");
    }

    private static void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did something when work completed.");
    }

    private static void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        DoWork((int)e.Argument);
    }    
    
    
    private static void DoWork(int workNo)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}.");
        Thread.Sleep(3000);
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}.");
    }
}

實際上,我們可以利用AsyncOperationManager實現自己的非同步物件,可以使用dnSpy對BackgroundWorker進行反編譯觀察具體的實現過程。

基於任務的非同步樣式TAP

在《C#並行程式設計(4):基於任務的並行》中,我們已經總結過TaskTask的用法,這裡主要關註的是C#的async/await語法與Task的結合用法。

在C#中,我們使用async標記定義一個非同步方法,使用await來等待一個非同步操作。簡單的用法如下:

public async Task DoWorkAsync()
{
    await Task.Delay(1000);
}

public async Task<int> DoWorkAndGetResultAsync()
{
    await Task.Delay(1000);
    return 1;
}

async/await編寫非同步過程很方便,但非同步方法的執行過程是怎樣呢?下麵的例子展示了一個非同步操作的呼叫過程,我們以這個例子來分析非同步方法的呼叫過程。

public static async Task Run()
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work.");

    Task workTask1 = DoWork(1);
    
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask1.Id} by async call.");

    Task workTask2 = DoWork(2);
    await workTask2;
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask2.Id} by async call.");

    Task workTask3 = DoWork(3);
    await workTask3;
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask3.Id} by async call.");

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work.");
}






private static async Task DoWork(int workNo)
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}.");
    DateTime now = DateTime.Now;
    await Task.Run(() =>
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} was running by task #{Task.CurrentId} with thread #{Thread.CurrentThread.ManagedThreadId}.");
        while (now.AddMilliseconds(3000) > DateTime.Now)
        {
        }
    });
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}.");
}

先來看一下例子的輸出:

19:07:33.032779=> thread #10 will do some work.
19:07:33.039762=> work #1 started with thread #10.
19:07:33.075664=> thread #10 got task #2 by async call.
19:07:33.075664=> work #2 started with thread #10.
19:07:33.078658=> work #2 was running by task #3 with thread #11.
19:07:33.082647=> work #1 was running by task #1 with thread #6.
19:07:36.040739=> work #1 done with thread #6.
19:07:36.077638=> work #2 done with thread #11.
19:07:36.077638=> thread #11 got task #4 by async call.
19:07:36.077638=> work #3 started with thread #11.
19:07:36.077638=> thread #11 got task #7 by async call.
19:07:36.077638=> thread #11 done the work.
19:07:36.077638=> work #3 was running by task #6 with thread #12.
19:07:39.077652=> work #3 done with thread #12.

在上面的輸出中,我們單看work #1,它由thread #10啟動,計算過程在thread #6中執行並結束,最後任務在thread #10中傳回,這裡我們沒有使用await來等待work #1的非同步任務;假如我們使用await等待非同步任務,如work #2,它在thread #10中啟動,計算過程在thread #11中執行並結束,任務最後在thread #11中傳回。大家可能發現了兩者的不同:await改變了Run()方法的執行執行緒,從DoWork()方法的執行也能夠看出,await會改變非同步方法的執行執行緒!

實際上,編譯器會把非同步方法轉換成狀態機結構,執行到await時,編譯器把當前正在執行方法(任務)掛起,當await的任務執行完成時,編譯器再恢復掛起的方法,所以我們的輸出中,非同步方法await前面和後面的程式碼,一般是在不同的執行緒中執行的。編譯器透過這種狀態機的機制,使得等待非同步操作的過程中執行緒不再阻塞,進而增強響應性和執行緒利用率。

理解非同步方法的執行機制後,相信對非同步的應用會變得更加嫻熟,這裡就不再總結非同步的具體用法。

已同步到看一看
贊(0)

分享創造快樂