C#並行編程(5):須要知道的異步

異步與並行的聯繫

你們知道「並行」是利用CPU的多個核心或者多個CPU同時執行不一樣的任務,咱們不關心這些任務之間的依賴關係。
可是在咱們實際的業務中,不少任務之間是相互影響的,好比統計車間整年產量的運算要依賴於各月產量的統計結果。假如你想在計算月產量的時候作些其餘事情,如導出生產異常報表,「異步」就能夠登上舞臺了。html

說到異步,必需要先提一下同步。一圖勝千言:編程

同步與異步

圖中操做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線程中,咱們要保持界面的響應性,耗時的操做最好都使用異步的方式執行。async

.NET提供了三種異步模式:異步編程

  • IAsyncResult模式(APM)
  • 基於事件的異步模式(EAP)
  • 基於任務的異步模式(TAP)

其中基於任務的異步模式是.NET推薦的異步編程方式。函數

IAsyncResult異步模式APM

下面是IAsyncResult基於委託的用法。線程

/// <summary>
/// 作做業的委託
/// </summary>
/// <param name="workNo">做業編號</param>
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);
    //result.AsyncWaitHandle.WaitOne();
    caller.EndInvoke(result);
    DoWork(3);
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work.");
}

/// <summary>
/// 作做業 
/// </summary>
/// <param name="workNo">做業編號</param>
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()來等待異步做業完成,一樣會阻塞當前線程。此外,能夠爲異步做業增長回調,異步做業在完成時會執行回調函數。code

基於事件的異步模式EAP

事件你們不會陌生,咱們在Winform編程的時候,總會用到事件。下面是利用BackgroundWorker實現的一個基於事件的簡單異步過程。咱們給異步對象(這裏是BackgroundWorker)訂閱DoWorkRunWorkCompleted事件,當調用RunWorkerAsync時,觸發異步對象的工做事件,此時會開闢一個新線程來執行目標操做。目標操做完成時,觸發工做完成事件,執行後續操做。與IAsyncResult模式不一樣的是,做業完成後的後續操做會在另外的一個線程執行,而IAsyncResult模式中,完成回調會在目標操做的執行線程中執行。orm

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);
    }

    /// <summary>
    /// 作做業 
    /// </summary>
    /// <param name="workNo">做業編號</param>
    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<T>的用法,這裏主要關注的是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); // 不使用await調用的異步方法,與正常方法同樣
    //await workTask1;
    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.");
}

/// <summary>
/// 異步做業
/// </summary>
/// <param name="workNo">做業編號</param>
/// <returns>異步任務</returns>
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前面和後面的代碼,通常是在不一樣的線程中執行的。編譯器經過這種狀態機的機制,使得等待異步操做的過程當中線程再也不阻塞,進而加強響應性和線程利用率。

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

相關文章
相關標籤/搜索