多線程是我們日常開發中或多或少都會遇到的,它涉及了很多概念:安全執行緒/競爭條件、鎖/死結檢測、同步/非同步、阻塞/非阻塞、訊號量/事件、線程池、生產消費模型等等。面對這些紛繁複雜的東西你是不是有點頭大(如果你現在沒有以後也會有的),你難道不想找尋一種可以讓自己輕鬆點的方法?反正我是一直在思索嘗試,直到最近瞭解了Erlang、Stackless Python並開始研究協程、微線程以後,我才有種豁然開朗的感覺。
你可能要問了,“協程、微線程”這名詞聽起來挺酷的但是它們比起傳統的多線程開發好在那裡呢?這個問題我確實很難回答出來,也許只有經曆過的人能感同身受。不過我倒是可以說說傳統開發方式的一些弊端,讓你自己來做個判斷。
1. 安全執行緒/臨界條件:這是一個很讓人討厭的東西一不小心就會出錯而且還很難尋找。也許你忘了在操作一個靜態(全域)的List或Hashtab時加鎖,亦或是你在一個非同步回呼函數中再次調用了當前線程的方法造成了死結。如果這些問題在產品上線以後不規律出現那你可以慘了。
2.同步/非同步:在同步且阻塞的時候你要自己開線程或者使用線程池,而非同步時候要使用回呼函數或者非同步委託(.net的Delegate)來處理。而無論同步、非同步其實都是藉助其他線程來處理,等處理完了就需要你來做一些線程同步的事情。比如通知主線程或對結果處理後再次做非同步處理等。這時你就要藉助semaphore/event(不是.net中的事件)、join等來幫你完成,這實在是勞心費力的事情。
3.效能:其實這個問題才是最困難的,因為線程這個東西是核心對象開多了再好的機器也負載不了,開少了無法滿足需求。所以這時候你就要去學習一些並行存取模型來解決這個問題。比如做tcp服務端程式你不可能使用一個線程單獨去處理一個socket請求,而可能的方法是用一個線程來監控一組socket(select檢測),然後對真正需要I/O的再單獨開線程處理。
上面這三條是我以前開發時經常遇到的問題也許不夠有代表性但我想表達的觀點是:多線程開發涉及的知識點很多而且有些東西需要不斷積累經驗才能做好。而如果我們能合理的剔除一些關注點那就有可能大幅度降低開發的複雜度。好了終於要切入主題了,我之所以寫了上面那麼一大堆廢話的目的就是為了讓大家能夠更清晰的和微線程、協程方式做比較。廢話少說讓我們繼續。
先來說說協程(coroutine),它是指兩個子過程通過相互協作完成某個任務。對應到實際代碼就是我們經常使用的yield,而更通用的概念就是迭代器模式。也許你會奇怪我們平時經常使用的for/foreach就是協程?確實如此,讓我用一個例子來說明。
//子過程1
class Enumerable : IEnumerable
{
public IEnumerator GetEnumerator()
{
return new Enumerator();
}
}
class Enumerator : IEnumerator
{
int _current;
public object Current
{
get { return _current; }
}
public bool MoveNext()
{
_current += 5;
if (_current < 100)
return true;
else
return false;
}
public void Reset()
{
throw new NotImplementedException();
}
}
//子過程2
foreach (var i in new Enumerable())
{
Console.WriteLine(i);
}
這個例子很簡單是吧,但是你不覺得它很像生產/消費模型嗎?Enmerator生產資料,然後在另一個子程式中使用foreach來迭代這些資料並作出處理。而如果這兩個子程式是跑在獨立線程中,是不是就感覺和多線程開發有點關係了。當然對於現在的大多數語言來說都提供了yield關鍵字它可以簡化迭代器的編寫。上面的代碼簡化後如下所示:
//子過程1
IEnumerable Enumerable()
{
var current = 0;
while(current < 100)
{
current += 5;
yield return current;
}
}
//子過程2
foreach (var i in new Enumerable())
{
Console.WriteLine(i);
}
我理解的協程概念就這麼多下面來介紹微線程。你可以把它理解為相對於線程更輕量級的邏輯線程,這就好比線程相對於進程來說更輕量級不知道這算不算是纖程的概念。總之微線程都是邏輯上的概念而真實啟動並執行只有一個線程。我們知道在同一時間片只能有一個線程針對一個cpu執行指令,而且其他的線程必須被掛起。然後核心發送器不斷的喚醒/掛起線程來類比多個任務的執行,而如果對線程使用不當就會造成過多的線程切換開銷。而實際上很多線程切換是沒有必要的,因為真正工作的線程在同一時間只有一個因此我們可以寫一個很簡單的調度來實現微線程之間的切換。這同時還帶了一個很大的好處就是微線程中的代碼永遠是安全執行緒的因為不會出現競爭條件了。但這同時引入了一個問題就是微線程中怎麼實現掛起/啟用操作,因為我們不可能在微線程中使用Sleep這種阻塞調用這會讓所有的微線程全部阻塞。在現有的語言基礎上解決這個問題的最簡單辦法就是使用協程,或者說使用迭代器模式,yield關鍵字。微線程中如果需要阻塞的時候就yield return 把把具體工作交給外部協作協程來處理,等處理完了再回到微線程中繼續執行yield return後面的代碼。這個過程在概念上與實際的線程掛起/喚醒是相同的,並且切換代價要小的多。這樣一來協程和微線程就很好的結合在一起,如果你沒理解也沒關係我在下篇文章中中將給出具體的實現源碼和樣本程式