C#異步編程由淺入深(一)

1、什麼算異步?

  廣義來說,兩個工做流能同時進行就算異步,例如,CPU與外設之間的工做流就是異步的。在面向服務的系統中,各個子系統之間通訊通常都是異步的,例如,訂單系統與支付系統之間的通訊是異步的,又如,在現實生活中,你去館子吃飯,工做流是這樣的,點菜->下單->作你的事->上菜->吃飯,這個也是異步的,具體來說你和廚師之間是異步的,異步是如此重要,因外它表明者高效率(二者或二者以上的工做能夠同時進行),但複雜,同步的世界簡單,但效率極極低。數據庫

2、在編程中的異步

  在編程中,除了同步和異步這兩個名詞,還多了一個阻塞和非阻塞,其中,阻塞和非阻塞是針對線程的概念,那麼同步和異步是針對誰呢?其實不少狀況下同步和異步並無具體針對某一事物,因此致使了針對同步阻塞、同步非阻塞、異步阻塞、異步非阻塞這幾個概念的模糊不清。而且也確實沒有清晰的邊界,請看如下例子:編程

public static void DoWorkA()
    {
        Thread thread = new Thread(() => 
        {
            Console.WriteLine("WorkA Done!");
        });
        thread.Start();
    }

    public static void DoWordB()
    {
        Thread thread = new Thread(() =>
        {
            Console.WriteLine("WorkB Done!");
        });
        thread.Start();
    }
    static void Main(string[] args)
    {
        DoWorkA();
        DoWordB();
    }

  假設運行該代碼的CPU是單核單線程,那麼請問?DoWorkA()、DoWorkB()這兩個函數是異步的嗎?由於CPU是單核,因此根本不能同時運行兩個函數,那麼從這個層次來說,他們之間實際上是同步的,可是,現實的狀況是咱們通常都認爲他們之間是異步的,由於咱們是從代碼的執行順序角度考慮的,而不是從CPU自己的工做流程考慮的。因此要分上下文考慮。再請看下面這個例子:c#

static void Main(string[] args)
    {
        DoWorkA();
        QueryDataBaseSync();//同步查詢數據庫
        DoWorkB();
    }

  從代碼的執行順序角度考慮,這三個函數執行就是同步的,可是,從CPU的角度來說,數據庫查詢工做(另外一臺機器)和CPU計算工做是異步的,在下文中,沒有作特別申明,則都是從代碼的執行順序角度來討論同步和異步。
  再解釋一下阻塞和非阻塞以及相關的知識:網絡

  阻塞特指線程由運行狀態轉換到掛起狀態,但CPU並不會阻塞,操做系統會切換另外一個處於就緒狀態的線程,並轉換成運行狀態。致使線程被阻塞的緣由有不少,如:發生系統調用(應用程序調用系統API,若是調用成功,會發生從應用態->內核態->應用態的轉換開銷),但此時外部條件並無知足,如從Socket內核緩衝區讀數據,此時緩衝區尚未數據,則會致使操做系統掛起該線程,切換到另外一個處於就緒態的線程而後給CPU執行,這是主動調用致使的,還有被動致使的,對於如今的分時操做系統,在一個線程時間片到了以後,會發生時鐘中斷信號,而後由操做系統預先寫好的中斷函數處理,再按必定策略(如線程優先級)切換至另外一個線程執行,致使線程被動地從運行態轉換成掛起狀態。
  非阻塞通常指函數調用不會致使執行該函數的線程從運行態轉換成掛起狀態。多線程

3、原始的異步編程模式之回調函數

  在此以前,咱們先稍微瞭解下圖形界面的工做原理,GUI程序大概能夠用如下僞代碼表示:異步

While(GetMessage() != 'exit') //從線程消息隊列中獲取一個消息,線程消息隊列由系統維護,例如鼠標移動事件,這個事件由操做系統捕捉,並投遞到線程的消息隊列中。
{
    msg = TranslateMessage();//轉換消息格式
    DispatherMessage(msg);//分發消息到相應的處理函數
}

  其中DispatherMessage根據不一樣的消息類型,調用不一樣的消息處理函數,例如鼠標移動消息(MouseMove),此時消息處理函數能夠根據MouseMove消息中的值,作相應的處理,例如調用繪圖相關函數畫出鼠標此刻的形狀。
  通常來說,咱們稱這個循環爲消息循環(事件循環、EventLoop),編程模型稱爲消息驅動模型(事件驅動),在UI程序中,執行這部分代碼的線程通常只有一個線程,稱爲UI線程,爲何是單線程,讀者能夠去思考。
  以上爲背景知識。如今,咱們思考,假如在UI線程中執行一個會致使UI線程被阻塞的操做,或者在UI線程執行一個純CPU計算的工做,會發生什麼樣的結果?若是執行一個致使UI線程被阻塞的操做,那麼這個消息循環就會被迫中止,致使相關的繪圖消息不能被相應的消息處理函數處理,表現就是UI界面「假死」,直到UI線程被喚起。若是是純CPU計算的工做,那麼也會致使其餘消息不能被及時處理,也會致使界面「假死」現象。如何處理這種狀況?寫異步代碼。
  咱們先用控制檯程序模擬這個UI程序,後面以此爲基礎。async

public static string GetMessage()
    {
        return Console.ReadLine();
    }

    public static  string TranslateMessage(string msg)
    {
        return msg;
    }

    public static  void DispatherMessage(string msg)
    {
        switch (msg)
        {
            case "MOUSE_MOVE":
                {
                    OnMOUSE_MOVE(msg);
                    break;
                }
            default:
                break;
        }
    }

    public static void OnMOUSE_MOVE(string msg)
    {
        Console.WriteLine("開始繪製鼠標形狀");
    }


    static void Main(string[] args)
    {
        while(true)
        {
            string msg = GetMessage();
            if (msg == "quit") return;
            string m = TranslateMessage(msg);
            DispatherMessage(m);
        }
    }
一、回調函數

  上面那個例子,一但外部有消息到來,根據不一樣的消息類型,調用不一樣的處理函數,如鼠標移動時產生MOUSE_DOWN消息,相應的消息處理函數就開始從新繪製鼠標的形狀,這樣一但你鼠標移動,就你會發現屏幕上的鼠標跟着移動了。
  如今假設咱們增長一個消息處理函數,如OnMOUSE_DOWN,這個函數內部進行了一個阻塞的操做,如發起一個HTTP請求,在HTTP請求回覆到來前,該UI程序會「假死」,咱們編寫異步代碼來解決這個問題。異步編程

public static int Http()
    {
        Thread.Sleep(1000);//模擬網絡IO延時
        return 1;
    }
    public static void HttpAsync(Action<int> action,Action error)
    {
        //這裏咱們用另外一個線程來實現異步IO,因爲Http方法內部是經過Sleep來模擬網絡IO延時的,這裏也只能經過另外一個線程來實現異步IO
        //但記住,多線程是實現異步IO的一個手段而已,它不是必須的,後面會講到如何經過一個線程來實現異步IO。
        Thread thread = new Thread(() => 
        {
            try
            {
                int res = Http();
                action(res);
            }
            catch
            {
                error();
            }
    
        });

        thread.Start();
    }
    public static void OnMouse_DOWN(string msg)
    {
        HttpAsync(res => 
        {
            Console.WriteLine("請求成功!");
            //使用該結果作一些工做
        }, () => 
        {
            Console.WriteLine("請求發生錯誤!");
        });
    }

  此時界面再也不「假死」了,咱們看下代碼可讀性,感受還行,可是,若是再在回調函數裏面再發起相似的異步請求呢?(有人可能有疑問,爲何還須要發起異步請求,我發同步請求不行嗎?這都是在另外一個線程裏了。是的,在這個例子裏是沒問題的,但真實狀況是,執行回調函數的代碼,通常都會在UI線程,由於取得結果後須要更新相關UI組件上的界面,例如文字,而更新界面的操做都是放在UI線程裏的,如何把回調函數放到UI線程上執行,這裏不作討論,在.NET中,這跟同步上下文(Synchronization context)有關,後面會講到),那麼代碼會變成這樣函數

public static void OnMouse_DOWN(string msg)
    {
        HttpAsync(res => 
        {
            Console.WriteLine("請求成功!");
            //使用該結果作一些工做

            HttpAsync(r1 => 
            {
                //使用該結果作一些工做

                HttpAsync(r2 => 
                {
                    //使用該結果作一些工做
                }, () => 
                {

                });
            }, () => 
            {

            });
        }, () => 
        {
            Console.WriteLine("請求發生錯誤!");
        });
    }

  寫過JS的同窗可能很清楚,這叫作「回調地獄」,如何解決這個問題?JS中有Promise,而C#中有Task,咱們先用Task來寫這一段代碼,而後本身實現一個與Task功能差很少的簡單的類庫。oop

public static Task<int> HttpAsync()
    {
        return Task.Run(() => 
        {
            return Http();
        });
    }


    public static void OnMouse_DOWN(string msg)
    {
        HttpAsync()
            .ContinueWith(t => 
            {
                if(t.Status == TaskStatus.Faulted)
                {

                }else if(t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            })
            .ContinueWith(t => 
            {
                if (t.Status == TaskStatus.Faulted)
                {

                }
                else if (t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            })
            .ContinueWith(t => 
            {
                if (t.Status == TaskStatus.Faulted)
                {

                }
                else if (t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            });
    }

  是否是感受清爽了許多?這是編寫異步代碼的第一個躍進。下篇將會介紹,如何本身實現一個簡單的Task。後面還會提到C#中async/await的本質做用,async/await是怎麼跟Task聯繫起來的,怎麼把本身寫的Task庫與async/await連結起來,以及一個線程如何實現異步IO。   以爲有收穫的不妨點個贊,有支持纔有動力寫出更好的文章。

相關文章
相關標籤/搜索