關於非同步,其實是個老生常談的話題,也是各大公司面試常問的問題之一.本文就幾個點來介紹非同步解決的問題
注:對多執行緒的執行的基本機制要了解
1、介紹
有人可能會有疑問,為什麼並行,非得用非同步.多執行緒也已可以啊,多開兩個執行緒不就行了.
案例分析:現在有一個生活場景.需要煮飯(假設需要20分鐘-機器煮)、洗衣服(假設需要25分鐘-機器洗)、蒸菜(假設需要10分鐘-機器蒸).並且昨晚之後我需要告知哥們.
(1)、同步
如果採用同步的方式(我先煮飯,把米放到電飯煲裡,然後站在電飯煲前面等,什麼事都不能幹,一直等到飯煮好.接下去在洗衣服....以此類推),那麼我需要一件一件做,那麼我總共要花費20+25+10=55分鐘.顯然這種方式很蠢.例項程式碼如下:
class Program
{
static void Main(string[] args)
{
CookRice();
CookDish();
DoLaundry();
Console.WriteLine("哥們,全都搞定了");
Console.ReadKey();
}
/// <summary>
/// 煮飯
/// </summary>
static void CookRice()
{
Thread.Sleep(20 * 1000 * 60);
}
/// <summary>
/// 蒸菜
/// </summary>
static void CookDish()
{
Thread.Sleep(10 * 1000 * 60);
}
/// <summary>
/// 洗衣服
/// </summary>
static void DoLaundry()
{
Thread.Sleep(25 * 1000 * 60);
}
}
(2)、多執行緒優化
ok,上面的程式碼執行方式顯然是很蠢的,因為那樣會佔用我(執行緒)大量的時間.
注意:這裡要從執行時間這個角度去考慮,因為作為Web開發,一個使用者請求從開始到結束,是有時間限制的,如果你一個請求(報表之類的特殊業務除外,指網際網路場景)超過1秒,使用者可能都無法忍受。 這裡的請求在.net下指代執行緒.
ok,那隻能對這個耗時任務,進行拆解,當然前提是他可以拆解(存在並行化的可能),我們這個例子顯然是可以的,於是,我這麼做,先把米放到電飯煲裡面,不在停留,接著立馬把衣服放到洗衣機,最後再把菜放到蒸鍋裡.然後我去幹別的事情了.程式碼如下:
class Program
{
static void Main(string[] args)
{
var t1=Task.Run(() => CookRice());
var t2 = Task.Run(() => CookDish());
var t3 = Task.Run(() => DoLaundry());
Task.WaitAll(t1, t2, t3);
Console.WriteLine("哥們,全部搞定了");
Console.ReadKey();
}
/// <summary>
/// 煮飯
/// </summary>
static void CookRice()
{
Thread.Sleep(20 * 1000 * 60);
}
/// <summary>
/// 蒸菜
/// </summary>
static void CookDish()
{
Thread.Sleep(10 * 1000 * 60);
}
/// <summary>
/// 洗衣服
/// </summary>
static void DoLaundry()
{
Thread.Sleep(25 * 1000 * 60);
}
}
ok,這裡我們將大任務,拆分成了三個小任務,分別交給了三個執行緒去做.同時,我等待三個任務完成之後,告訴我哥們.這是整個任務的執行時間大大縮短,相當於原先一個人的活,交給了三個人幹.能不快嗎!
ok,到這裡很多人覺得這樣就行了.已經無法再繼續優化了,這時候非同步登場了.
(3)、非同步優化
再優化程式碼之前,得知道執行緒池和CLR的概念,每個CLR會維護一個執行緒池.既然是池,說明執行緒的數量是有限的.並且我們的Web應用程式所使用的執行緒都會從CLR中去調取.那就說明,我們的Web程式能使用的執行緒有限.
ok.再回到上面的程式碼,
Task.WaitAll會阻塞主執行緒,主執行緒會在這裡休眠,意味著這三個任務不做完,主執行緒會一直被佔用.對應生活場景,就是我一直看著三臺機器的執行,知道完成之後告訴我哥們.這期間我幹不了任何事,只能看著.
那問題就大了.如果在高併發場景下.瞬時發起了1000條請求,那麼就會產生非常多的等待執行緒,這些執行緒啥都不幹,就乾等著.造成了嚴重的資源浪費.顯然是有問題的.
ok,非同步登場了.
非同步的原理(程式碼層面的介紹請百度),大致是這樣,所有的執行緒不在等待,阻塞而是通過執行緒池排程,就是執行緒池主動通知.程式碼如下:
class Program
{
static async void Main(string[] args)
{
Console.WriteLine($"當前是我在工作");
var t1=Task.Run(() => CookRice());
var t2 = Task.Run(() => CookDish());
var t3 = Task.Run(() => DoLaundry());
Console.WriteLine($"我觸發了await操作,就返回上一個呼叫方法去幹別的事情去了,同時通過狀態機機制(自行百度),這個方法會被暫停");
await Task.WhenAll(t1, t2, t3);
Console.WriteLine("await 內部操作執行完畢,執行緒池委派了一個新執行緒來執行接下去的任務,狀態機機制又會恢復當前方法,接下去執行");
Console.WriteLine("哥們,搞定了,但我不是你哥們,我是你哥們的朋友");
Console.ReadKey();
}
/// <summary>
/// 煮飯
/// </summary>
static void CookRice()
{
Thread.Sleep(20 * 1000 * 60);
}
/// <summary>
/// 蒸菜
/// </summary>
static void CookDish()
{
Thread.Sleep(10 * 1000 * 60);
}
/// <summary>
/// 洗衣服
/// </summary>
static void DoLaundry()
{
Thread.Sleep(25 * 1000 * 60);
}
}
ok,上面的程式碼簡要的闡述了非同步的原理,通過async await程式設計模型,當Main方法執行到await之前時,我(主執行緒)就會回到上一個呼叫方法接著執行別的任務,如果沒有返回執行緒池.接著通過狀態機機制,暫停當前方法的執行,當await方法執行完畢時,執行緒池會委派新的執行緒回來接著執行接下去的方法,再次之前狀態機會恢復方法的執行.以此類推.
通過這種方式(非同步),我們的Web程式就能高效率的利用好執行緒.
(4)、非同步在磁碟IO和網路請求上面的優勢
同步程式在處理磁碟IO和網路請求時,同樣會採用阻塞的方式,比如發起一個後端http、webscoket請求(使用同步方法)、檔案讀寫請求等等,那麼主執行緒等等到遠端主機和硬體裝置響應之後接著執行,期間他不會返回,會一直等.那麼這和上面的問題是一樣的了.這就是所謂處理IO-Bound Operation的方式,很顯然,這也是一個非同步操作。當我們希望進行一個非同步的IO-Bound Operation時,CLR會(通過Windows API)發出一個IRP(I/O Request Packet)。當裝置準備妥當,就會找出一個它“最想處理”的IRP(例如一個讀取離當前磁頭最近的資料的請求)並進行處理,處理完畢後裝置將會(通過Windows)交還一個表示工作完成的IRP。CLR會為每個程式建立一個IOCP(I/O Completion Port)並和Windows作業系統一起維護。IOCP中一旦被放入表示完成的IRP之後(通過內部的ThreadPool.BindHandle完成),CLR就會盡快分配一個可用的執行緒用於繼續接下去的任務。這種做法的需要一個重要條件,這就是發出用於請求的IRP的操作能夠立即返回,並且這個IO操作不會使用任何執行緒。而此時,這種非同步呼叫是真正地在節省資源,因為我們可以騰出執行緒用來處理其他任務了,但是這種做法據說需要作業系統和裝置的支援,但是我實際測試發現使用非同步Api的收益明顯要高於同步.
2、總結
綜上所述,非同步的優勢已經非常明顯了,並且Web開發,基本都是要麼和tcp要麼和磁碟打交道.所以用非同步個人認為是最佳實踐.