一、什么算異步?
廣義來講,兩個工作流能同時進行就算異步,例如,CPU與外設之間的工作流就是異步的。在面向服務的系統中,各個子系統之間通信一般都是異步的,例如,訂單系統與支付系統之間的通信是異步的,又如,在現實生活中,你去館子吃飯,工作流是這樣的,點菜->下單->做你的事->上菜->吃飯,這個也是異步的,具體來講你和廚師之間是異步的,異步是如此重要,因外它代表者高效率(兩者或兩者以上的工作可以同時進行),但復雜,同步的世界簡單,但效率極極低。
二、在編程中的異步
在編程中,除了同步和異步這兩個名詞,還多了一個阻塞和非阻塞,其中,阻塞和非阻塞是針對線程的概念,那么同步和異步是針對誰呢?其實很多情況下同步和異步並沒有具體針對某一事物,所以導致了針對同步阻塞、同步非阻塞、異步阻塞、異步非阻塞這幾個概念的模糊不清。並且也確實沒有清晰的邊界,請看以下例子:
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本身的工作流程考慮的。所以要分上下文考慮。再請看下面這個例子:
static void Main(string[] args)
{
DoWorkA();
QueryDataBaseSync();//同步查詢數據庫
DoWorkB();
}
從代碼的執行順序角度考慮,這三個函數執行就是同步的,但是,從CPU的角度來講,數據庫查詢工作(另一台機器)和CPU計算工作是異步的,在下文中,沒有做特別申明,則都是從代碼的執行順序角度來討論同步和異步。
再解釋一下阻塞和非阻塞以及相關的知識:
阻塞特指線程由運行狀態轉換到掛起狀態,但CPU並不會阻塞,操作系統會切換另一個處於就緒狀態的線程,並轉換成運行狀態。導致線程被阻塞的原因有很多,如:發生系統調用(應用程序調用系統API,如果調用成功,會發生從應用態->內核態->應用態的轉換開銷),但此時外部條件並沒有滿足,如從Socket內核緩沖區讀數據,此時緩沖區還沒有數據,則會導致操作系統掛起該線程,切換到另一個處於就緒態的線程然后給CPU執行,這是主動調用導致的,還有被動導致的,對於現在的分時操作系統,在一個線程時間片到了之后,會發生時鍾中斷信號,然后由操作系統預先寫好的中斷函數處理,再按一定策略(如線程優先級)切換至另一個線程執行,導致線程被動地從運行態轉換成掛起狀態。
非阻塞一般指函數調用不會導致執行該函數的線程從運行態轉換成掛起狀態。
三、原始的異步編程模式之回調函數
在此之前,我們先稍微了解下圖形界面的工作原理,GUI程序大概可以用以下偽代碼表示:
While(GetMessage() != 'exit') //從線程消息隊列中獲取一個消息,線程消息隊列由系統維護,例如鼠標移動事件,這個事件由操作系統捕捉,並投遞到線程的消息隊列中。
{
msg = TranslateMessage();//轉換消息格式
DispatherMessage(msg);//分發消息到相應的處理函數
}
其中DispatherMessage根據不同的消息類型,調用不同的消息處理函數,例如鼠標移動消息(MouseMove),此時消息處理函數可以根據MouseMove消息中的值,做相應的處理,例如調用繪圖相關函數畫出鼠標此刻的形狀。
一般來講,我們稱這個循環為消息循環(事件循環、EventLoop),編程模型稱為消息驅動模型(事件驅動),在UI程序中,執行這部分代碼的線程一般只有一個線程,稱為UI線程,為什么是單線程,讀者可以去思考。
以上為背景知識。現在,我們思考,假如在UI線程中執行一個會導致UI線程被阻塞的操作,或者在UI線程執行一個純CPU計算的工作,會發生什么樣的結果?如果執行一個導致UI線程被阻塞的操作,那么這個消息循環就會被迫停止,導致相關的繪圖消息不能被相應的消息處理函數處理,表現就是UI界面“假死”,直到UI線程被喚起。如果是純CPU計算的工作,那么也會導致其他消息不能被及時處理,也會導致界面“假死”現象。如何處理這種情況?寫異步代碼。
我們先用控制台程序模擬這個UI程序,后面以此為基礎。
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);
}
}
1、回調函數
上面那個例子,一但外部有消息到來,根據不同的消息類型,調用不同的處理函數,如鼠標移動時產生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功能差不多的簡單的類庫。
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。
覺得有收獲的不妨點個贊,有支持才有動力寫出更好的文章。