版權聲明:本文由韓偉原創文章,轉載請註明出處:
文章原文連接:https://www.qcloud.com/community/article/166java
來源:騰雲閣 https://www.qcloud.com/community程序員
之前咱們的代碼,從上往下執行,每一行都會佔用必定的CPU時間,這些代碼的直接順序,也是和編寫的順序基本一致,任何一行代碼,都是惟一時刻的執行任務。當咱們在編寫分佈式程序的時候,咱們的代碼將再也不好像那些單進程、單線程的程序同樣簡單。咱們要把同時運行的不一樣代碼,在同一段代碼中編寫。就好像咱們要把整個交響樂團的每一個樂器的曲譜,所有寫到一張紙上。爲了解決這種編程的複雜度,業界發展出了多種編碼形式。編程
在多進程的編碼模型上,fork()函數能夠說一個很是典型的表明。在一段代碼中,fork()調用以後的部分,可能會被新的進程中執行。要區分當前代碼的所在進程,要靠fork()的返回值變量。這種作法,等於把多個進程的代碼都合併到一塊,而後經過某些變量做爲標誌來劃分。這樣的寫法,對於不一樣進程代碼大部份相同的「同質進程」來講,仍是比較方便的,最怕就是有大量的不一樣邏輯要用不一樣的進程來處理,這種狀況下,咱們就只能本身經過規範fork()附近的代碼,來控制混亂的局面。比較典型的是把fork()附近的代碼弄成一個相似分發器(dispatcher)的形式,把不一樣功能的代碼放到不一樣的函數中,以fork以前的標記變量來決定如何調用。
動態多進程的代碼模式
在咱們使用多線程的API時,狀況就會好不少,咱們能夠用一個函數指針,或者一個帶回調方法的對象,做爲線程執行的主體,而且以句柄或者對象的形式來控制這些線程。做爲開發人員,咱們只要掌握了對線程的啓動、中止等有限的幾個API,就能很好的對並行的多線程進行控制。這對比多進程的fork()來講,從代碼上看會更直觀,只是咱們必需要分清楚調用一個函數,和新建一個線程去調用一個函數,之間的差異:新建線程去調用函數,這個操做會很快的結束,並不會依序去執行那個函數,而是表明着,那個函數中的代碼,可能和線程調用以後的代碼,交替的執行。promise
因爲多線程把「並行的任務」做爲一個明確的編程概念定義了出來,以句柄、對象的形式封裝好,那麼咱們天然會但願對多線程能更多複雜而細緻的控制。所以出現了不少多線程相關的工具。比較典型的編程工具備線程池、線程安全容器、鎖這三類。線程池提供給咱們以「池」的形態,自動管理線程的能力:咱們不須要本身去考慮怎麼創建線程、回收線程,而是給線程池一個策略,而後輸入須要執行的任務函數,線程池就會自動操做,好比它會維持一個同時運行線程數量,或者保持必定的空閒線程以節省建立、銷燬線程的消耗。在多線程操做中,不像多進程在內存上徹底是區分開的,因此能夠訪問同一分內存,也就是對堆裏面的同一個變量進行讀寫,這就可能產生程序員所預計不到的狀況(由於咱們寫程序只考慮代碼是順序執行的)。還有一些對象容器,好比哈希表和隊列,若是被多個線程同時操做,可能還會由於內部數據對不上,形成嚴重的錯誤,因此不少人開發了一些能夠被多個線程同時操做的容器,以及所謂「原子」操做的工具,以解決這樣的問題。有些語言如Java,在語法層面,就提供了關鍵字來對某個變量進行「上鎖」,以保障只有一個線程能操做它。多線程的編程中,不少並行任務,是有必定的阻塞順序的,因此有各類各樣的鎖被髮明出來,好比倒數鎖、排隊鎖等等。java.concurrent庫就是多線程工具的一個大集合,很是值得學習。然而,多線程的這些五花八門的武器,其實也是證實了多線程自己,是一種不太容易使用的順手的技術,可是咱們一會兒尚未更好的替代方案罷了。
安全
多線程的對象模型服務器
在多線程的代碼下,除了啓動線程的地方,是和正常的執行順序不一樣之外,其餘的基本都仍是比較近似單線程代碼的。可是若是在異步併發的代碼下,你會發現,代碼必定要裝入一個個「回調函數」裏。這些回調函數,從代碼的組織形態上,幾乎徹底沒法看出來其預期的執行順序,通常只能在運行的時候經過斷點或者日誌來分析。這就對代碼閱讀帶來了極大的障礙。所以如今有愈來愈多的程序員關注「協程」這種技術:能夠用相似同步的方法來寫異步程序,而無需把代碼塞到不一樣的回調函數裏面。協程技術最大的特色,就是加入了一個叫yield的概念,這個關鍵字所在的代碼行,是一個相似return的做用,可是又表明着後續某個時刻,程序會從yield的地方繼續往下執行。這樣就把那些須要回調的代碼,從函數中得以解放出來,放到yield的後面了。在不少客戶端遊戲引擎中,咱們寫的代碼都是由一個框架,以每秒30幀的速度在反覆執行,爲了讓一些任務,能夠分別放在各幀中運行,而不是一直阻塞致使「卡幀」,使用協程就是最天然和方便的了——Unity3D就自帶了協程的支持。cookie
在多線程同步程序中,咱們的函數調用棧就表明了一系列同屬一個線程的處理。可是在單線程的異步回調的編程模式下,咱們的一個回調函數是沒法簡單的知道,是在處理哪個請求的序列中。因此咱們每每須要本身寫代碼去維持這樣的狀態,最多見的作法是,每一個併發任務啓動的時候,就產生一個序列號(seqid),而後在全部的對這個併發任務處理的回調函數中,都傳入這個seqid參數,這樣每一個回調函數,均可以經過這個參數,知道本身在處理哪一個任務。若是有些不一樣的回調函數,但願交換數據,好比A函數的處理結果但願B函數能獲得,還能夠用seqid做爲key把結果存放到一個公共的哈希表容器中,這樣B函數根據傳入的seqid就能去哈希表中得到A函數存入的結果了,這樣的一份數據咱們每每叫作「會話」。若是咱們使用協程,那麼這些會話可能都不須要本身來維持了,由於協程中的棧表明了會話容器,當執行序列切換到某個協程中的時候,棧上的局部變量正是以前的處理過程的內容結果。數據結構
協程的代碼特徵多線程
爲了解決異步編程的回調這種複雜的操做,業界還發明瞭不少其餘的手段,好比lamda表達式、閉包、promise模型等等,這些都是但願咱們,能從代碼的表面組織上,把在多個不一樣時間段上運行的代碼,以業務邏輯的形式組織到一塊兒。閉包
最後我想說說函數式編程,在多線程的模型下,並行代碼帶來最大的複雜性,就是對堆內存的同時操做。因此咱們才弄出來鎖的機制,以及一大批對付死鎖的策略。而函數式編程,因爲根本不使用堆內存,因此就無需處理什麼鎖,反而讓整個事情變得很是簡單。惟一須要改變的,就是咱們習慣於把狀態放到堆裏面的編程思路。函數式編程的語言,好比LISP或者Erlang,其核心數據結果是鏈表——一種能夠表示任何數據結構的結構。咱們能夠把全部的狀態,都放到鏈表這個數據列車中,而後讓一個個函數去處理這串數據,這樣一樣也能夠傳遞程序的狀態。這是一種用棧來代替堆的編程思路,在多線程併發的環境下,很是的有價值。
分佈式程序的編寫,一直都伴隨着大量的複雜性,影響咱們對代碼的閱讀和維護,因此咱們纔有各類各樣的技術和概念,試圖簡化這種複雜性。也許咱們沒法找到任何一個通用的解決方案,可是咱們能夠經過理解各類方案的目標,來選擇最適合咱們的場景:
動態多進程fork——同質的並行任務
多線程——能明確劃的邏輯複雜的並行任務
異步併發回調——對性能要求高,但中間會被阻塞的處理較少的並行任務
協程——以同步的寫法編寫併發的任務,可是不合適發起複雜的動態並行操做。
函數式編程——以數據流爲模型的並行處理任務
分佈式的編程中,對於CPU時間片的切分自己不是難點,最困難的地方在於並行的多個代碼片斷,如何進行通訊。由於任何一個代碼段,都不可能徹底單獨的運做,都須要和其餘代碼產生必定的依賴。在動態多進程中,咱們每每只能經過父進程的內存提供共享的初始數據,運行中則只能經過操做系統間的通信方式了:Socket、信號、共享內存、管道等等。不管那種作法,這些都帶來了一堆複雜的編碼。這些方式大部分都相似於文件操做:一個進程寫入、另一個進程讀出。因此不少人設計了一種叫「消息隊列」的模型,提供「放入」消息和「取出」消息的接口,底層則是能夠用Socket、共享內存、甚至是文件來實現。這種作法幾乎可以處理任何情況下的數據通信,並且有些還能保存消息。可是缺點是每一個通訊消息,都必須通過編碼、解碼、收包、發包這些過程,對處理延遲有必定的消耗。
若是咱們在多線程中進行通訊,那麼咱們能夠直接對某個堆裏面的變量直接進行讀寫,這樣的性能是最高的,使用也很是方便。可是缺點是可能出現幾個線程同時使用變量,產生了不可預期的結果,爲了對付這個問題,咱們設計了對變量的「鎖」機制,而如何使用鎖又成爲另一個問題,由於可能出現所謂的「死鎖」問題。因此咱們通常會用一些「線程安全」的容器,用來做爲多線程間通信的方案。爲了協調多個線程之間的執行順序,還可使用不少種類型的「工具鎖」。
在單線程異步併發的狀況下,多個會話間的通訊,也是能夠經過直接對變量進行讀寫操做,並且不會出現「鎖」的問題,由於本質上每一個時刻都只有一個段代碼會操做這個變量。然而,咱們仍是須要對這些變量進行必定規劃和整理,不然各類指針或全局變量在代碼中散佈,也是很出現BUG的。因此咱們通常會把「會話」的概念變成一個數據容器,每段代碼均可以把這個會話容器做爲一個「收件箱」,其餘的併發任務若是須要在這個任務中通信,就把數據放入這個「收件箱」便可。在WEB開發領域,和cookie對應的服務器端Session機制,就是這種概念的典型實現。