併發編程的模型

併發編程的模型

 

併發是多核編程中非常困難的部分,主要原因是多個CPU,但是共享一個內存,所以必須有一套機制保證這些CPU不會衝突。

 

理論上一個應用程序綁定一個CPU,然後從頭執行到尾是最高效的方式,然而實際中的應用,總是會相互依賴,或者依賴某個低速的IO操作,這時候這些應用就會等待。等待的時候能高效的將CPU出讓給別人是很重要的。

 

爲了併發且保護共享的數據結構,很多的方式被髮明出來,例如鎖,互斥變量,信號量等。這些方式解決的是可以同時執行的多個程序直接的協調問題。這些協調工具側重點各不同,而且出現衝突的時候的處理也不同,稍不注意就可能出現死鎖,或者效率低下的問題。可以說併發編程爲啥困難,主要就是如何利用這些工具保證程序運行的正確性和性能非常的複雜。

 

抽象來看,程序可以分割成多個原子部分,每個原子部分不會等待別人,從而能夠高效的利用CPU,而這些部分之間,是互相依賴的。這個依賴就兩種:

  1. 等待訪問公共的內存;
  2. 等待訪問外部的慢IO。

 

當處理衝突的時候,系統提供的常用方式,例如鎖,信號量的依據主要是進程和線程。當發生不滿足的條件的時候,會將進程或者線程丟到操作系統的調度隊列中去等待,從而引發上下文的切換。我們知道操作系統上下文切換是非常耗時的,所以很難支持特別高的併發處理能力。利用這種方式設計出來的程序運行的時候,CPU利用率不高,且能看到大量的上下文切換。

 

假設常見的3GHZ CPU一次進程上下文切換時間爲5us,則一秒鐘如果切換超過20w次就會消耗一個CPU核。

 

針對等待IO的操作,是必須等待的,所以這個時候如果能快速的將CPU切換到別的需要CPU的程序中,則能大量的節省CPU消耗,從而提高併發的能力。協程主要就是利用幹這個事情的,它將IO的操作封裝掉,調用IO訪問的時候如果發現不滿足條件,則直接切換到可運行的協程上,由於是進程內部的快速切換,需要保持的現場特別少,並沒有操作系統的其他開銷所以特別高效。

 

針對訪問公共內存導致的等待,能採取的辦法就是減少共同訪問的內存區域。對於實在是避免不了的互相協調,那也避免太頻繁的切換。這種場景下,空間換時間是一個比較好的選擇,也就是說兩個程序協調的時候,如果每個處理的數據都協調並等待對方處理,這就意味着每次都需要進行上下文切換,如果大家將這種協調放在一個隊列中,雙方就可以依賴這個隊列進行解耦,一方可以處理很多的數據放在隊列中,而另一方等到有機會的時候就可以處理一批數據,從而減少切換(注:這個隊列本身也是共享的數據,所以對這個隊列的訪問需要注意衝突的問題)。

 

Actor就是這樣一種編程模型,每段程序原子程序叫一個Actor,這個Actor有一個信箱(就是一個數據隊列),別人如果和它協調幹一個事情的話,就給它發一個消息。信箱的主人就會收到這個消息,並進行處理。並且由於這個信箱的存在,所以也能解決雙方處理速度不一致的匹配問題。

 

剛纔提到,協程可以解決等待IO的問題(底層將IO的等待都封裝掉),協程之間的通信或者說協調也可以使用類似的通知隊列來實現。在go語言等語言中,這種隊列稱之爲Channel,並且不像Actor模型(隊列是隱含的,每個Actor都有),寫的協程和讀的協程之間的隊列是單獨定義的,並且和協程之間沒有一對一的關係。如果將Actor的通知機制看成Queue的話,這種就是典型的Pub-Sub機制了。這種機制明細比Actor的那種更解耦,所以靈活性更大。但是帶來的副作用就是用不好更容易出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章