高永強
(武警工程大學信息工程學院,西安710086)
傳統(tǒng)并發(fā)編程一般采用多進程或者多線程的方式,需要考慮數據競爭、同步互斥、死鎖等等問題,使開發(fā)并發(fā)程序尤其是大型服務器程序的難度大大加大。大量的進程或線程創(chuàng)建不僅會占用大量內存,而且隨著線程數量的增多,多線程之間切換的開銷是不容忽視的,會浪費大量CPU時間在調度上[1]。在當今互聯網高并發(fā)場景下,迫切需要新的并發(fā)編程方式來滿足高并發(fā)的需要,同時降低并發(fā)程序的開發(fā)難度。
并發(fā)編程最大的困難就在與對共享資源的競爭,CSP(Communicating Sequential Process,通訊順序進程)和Actor模型都是基于消息傳遞的并發(fā)編程模型,它們的并發(fā)體之間不共享內存,由此可以在業(yè)務代碼層面實現無鎖并發(fā)。同時,CSP模型、Actor模型和協程模型中并發(fā)體的調度和切換發(fā)生在用戶態(tài),大幅降低了切換開銷。
CSP是貝爾實驗室的Tony Hoare在1978年提出的一種并發(fā)模型。CSP有著精確的數學模型,并實際應用在了Hoare參與設計的T9000通用計算機上。CSP模型中最重要的兩個概念是Process(進程)和Channel(通道)。這里的進程和傳統(tǒng)多進程編程中的進程有所區(qū)別,CSP中的進程是實際并發(fā)執(zhí)行的實體,是一種運行在用戶態(tài)的用戶線程,其調度不是由操作系統(tǒng)來完成的,而是由編程語言的運行時進行調度。CSP中通道是第一類對象,兩個獨立的并發(fā)實體通過共享的通道進行通信。在Java、C++、或者Python等語言的多線程編程中,線程間通信都是通過共享內存的方式來進行的,在訪問共享數據時,必須通過互斥鎖或信號量等機制確保數據的一致性。不同于傳統(tǒng)的多線程通過共享內存來通信,CSP講究的是“以通信的方式來共享內存”。在CSP模型中,程序就是一組無共享狀態(tài)進程的并行組合,進程間的通信和同步是通過Channel完成的。Process和Channel之間的關系如圖1所示。
圖1 Process和Channel之間的關系
Go語言是為并發(fā)而生的語言,Go語言是為數不多的在語言層面實現并發(fā)的語言。Go語言強大的并發(fā)編程能力,使其在微服務架構和云原生技術領域大放異彩,Docker、Kubernetes和etcd等軟件均是采用Go語言進行開發(fā)的[2]。Go語言中通過Goroutine(Go協程)與Channel實現了CSP模型中的核心概念Process和Channel。Process在go語言上的表現就是Gorou?tine,它是實際并發(fā)執(zhí)行的實體,每個實體之間是通過Channel通訊來實現數據共享。
(1)Goroutine的特點
不同于Python基于進程的并發(fā)模型,以及C++、Java等基于線程的并發(fā)模型,Go語言采用輕量級的Goroutine來實現并發(fā)。Goroutine是Go語言中并發(fā)的執(zhí)行單位,通過Go關鍵字可以簡單而快速地創(chuàng)建一個Goroutine。與線程相比,Goroutine具有以下特點:
●用戶態(tài)。Goroutine處于用戶態(tài),由Go語言調度器進行調度,避免了內核態(tài)和用戶態(tài)的切換導致的成本。Goroutine之間切換的開銷要比線程切換的開銷小得多,一個Go語言程序可以輕而易舉地創(chuàng)建成千上萬個Goroutine,而操作系統(tǒng)能夠創(chuàng)建的線程數量要少得多。
●輕量級。在一般的操作系統(tǒng)中,線程棧的大小是固定的且在運行過程中線程棧的大小不能伸縮,例如Linux系統(tǒng)默認線程棧大小是2MB。Goroutine默認棧要比線程棧小很多,一個Goroutine只占幾KB,并且Goroutine的棧是可伸縮的。
●通信靈活。Goroutine既可以通過共享內存進行通信,也可以通過Channel進行通信,給并發(fā)編程帶來了較大的靈活性。
(2)Goroutine的調度
Goroutine處于用戶態(tài),最終要在操作系統(tǒng)線程上執(zhí)行,Go語言調度器負責將多個Goroutine復用到線程上。調度器是線程和Goroutine的中間層,通過調度器的調度,每一個內核線程都能夠執(zhí)行多個Goroutine,并且在Goroutine進行一些I/O操作時及時切換,提高線程的利用率。Go目前使用的調度器是基于GMP模型重新設計的,其中G表示Goroutine,它是一個待執(zhí)行的任務;M表示操作系統(tǒng)的線程,它由操作系統(tǒng)的調度器調度和管理;P表示處理器,它可以被看做運行在線程上的本地調度器。
(3)Go Channel
(1)給出輸入信號x(t),設置迭代次數,通常情況下,迭代次數越高,分解越精確,但是同時所花時間也越長。將重建信號初始化置零。
作為Go核心的數據結構和Goroutine之間的通信方式,Channel是支撐Go語言高性能并發(fā)編程模型的重要結構。在很多主流的編程語言中,多個線程一般通過共享內存方式進行通信和傳遞數據。雖然在Go語言中也能使用共享內存加互斥鎖進行通信,但推薦的方式是通過Channel進行Goroutine之間的通信?!安灰ㄟ^共享內存來通信,要通過通信來共享內存”是Go語言重要的設計哲學。
Go語言中的Channel是一種隊列式的數據結構,遵循先入先出的規(guī)則。Go中Channel的容量可以為0,容量為0的Channel被稱為無緩沖的Channel,容量大于0的Channel被稱為有緩沖的Channel。對于無緩沖Channel,如果向Channel發(fā)送數據的Goroutine先被調用,則該Goroutine將被掛起直到接收數據的Gorou?tine被調用。同樣,如果接收數據的Goroutine先被調用,它將被掛起直到發(fā)送數據的Goroutine被調用。有緩沖的Channel和阻塞隊列非常類似,如果Channel已滿,那么向Channel發(fā)送數據的Goroutine將被掛起。反之,如果Channel為空,從Channel接收數據的Gor?outine將被掛起。Channel支持創(chuàng)建、接收、發(fā)送和關閉四個操作。與BlockingQueue不同的是,Channel可以被關閉,發(fā)送者關閉通道來表明沒有更多的元素將會進入通道。Go從語言層面保證同一個時間只有一個Goroutine能夠訪問Channel里面的數據?;谶@些特性,使用Channel可以輕松實現Goroutine之間的同步和互斥。
Actor模型是一種并發(fā)計算模型,其中的Actor是計算的基本單位,1973年Carl Hewitt在論文A Universal Modular Actor Formalism for Artificial Intelligence中首次提出Actor模型[3]。Actor模型由一個個稱為Actor的執(zhí)行體和Mailbox(郵箱)組成。在Actor理論中,一切都被認為是Actor,一個Actor實例是執(zhí)行計算的最小單元,擁有自己的狀態(tài)和行為,它能接收一個消息并且基于消息內容執(zhí)行計算任務。
Actor與Actor之間只能通過消息進行通信,一個Actor可以發(fā)送消息給其他Actor,也可以從其他Actor接收消息,如圖2所示。Actor模型內部的狀態(tài)由自己的行為維護,外部線程不能直接調用對象的行為,保證了Actor內部數據只有被自己修改。Actor的一大重要特征在于Actor之間相互隔離,它們并不互相共享內存,一個Actor能維持一個私有的狀態(tài)。由于Actor之間沒有共享數據,所以可以輕松實現無鎖并發(fā)。
圖2 Actor之間的通信方式
每個Actor都有一個屬于自己的信箱。Actor的信箱類似一個隊列,發(fā)送到Actor的消息依次存入目標Actor的信箱中等待處理。每個Actor是串行處理信箱中的消息的,這樣在Actor內部保證了不會出現并發(fā)安全問題。當一個Actor接收到消息后,它能做如下三件事中的一件:創(chuàng)建其他Actor;向其他Actor發(fā)送消息;指定當前的Actor如何處理下一個消息。這樣的設計解耦了Actor之間的關系,且發(fā)送消息時不會被阻塞。雖然所有Actor可以同時運行,但它們都按照信箱接收消息的順序來依次處理消息,且僅在當前消息處理完成后才會處理下一個消息。
Scala語言的Akka庫實現了Actor模型,其借鑒了Erlang的Actor模型實現,同時又引入了許多新的特性,為并發(fā)編程提供了強大的工具[4]。Akka是一款優(yōu)秀的分布式并發(fā)框架,由Scala語言開發(fā),運行于Java虛擬機之上,同時支持使用Scala、Java和Kotlin等基于JVM的語言進行編程。在Scala語言中仍然可以使用Java線程,但是使用Akka中的Actor模型是更好的選擇。Akka中的Actor是一個比線程更高層的抽象,最終是跑在Java的線程中的,多個Actor在底層可以共享一個線程。使用Akka進行并發(fā)編程可以不用擔心底層線程、鎖和共享數據沖突等傳統(tǒng)多線程編程面臨的問題。Akka提供了豐富的組件,比如郵箱、路由組件、持久化組件等,在底層對分布式和并行模式進行了高度且統(tǒng)一的抽象,使用很少的代碼就可以實現一個完整的高并發(fā)分布式應用[5]。
(1)Akka中Mailbox的并發(fā)安全機制
對于Mailbox存在兩個操作,一個是向Mailbox中寫入消息,一個是從Mailbox中讀取消息,因此可能會出現一條消息在被寫入Mailbox中還沒結束的時候,就被Actor讀取走的情況,這就會引發(fā)并發(fā)安全問題。所以Mailbox必須保證消息完整地寫入后才能被接收處理,即Mailbox必須是并發(fā)訪問安全的。Mailbox的底層數據結構是一個線程安全的存儲消息的隊列,Scala標準庫中的LinkedBlockingDeque和ConcurrentLinked?Queue等線程安全隊列均可以作為Mailbox的底層基礎結構。但Akka沒有采用這種方案,而是實現了一個AbstractNodeQueue數據結構,這種結構是一個功能更加明確的隊列,專門為Mailbox的需求所設計,在兼顧較高性能的同時,保證了上層Mailbox的并發(fā)訪問安全。
(2)Actor之間的消息傳遞
在Akka中,可以使用tell和ask兩種方式向Actor發(fā)送消息,它們都以異步的方式發(fā)送消息,不同的是,前者發(fā)完后立即返回,而后者會返回一個Future對象,假如在設置的時間內沒有得到返回結果,消息的發(fā)送方會收到一個超時異常。
(3)Akka中Actor的層級關系
如圖3所示,Actor系統(tǒng)從上到下有嚴格的層級關系。與父進程可以創(chuàng)建子進程類似,一個Actor也可以創(chuàng)建多個子Actor,最終形成一種樹形結構。當子Ac?tor在處理消息時發(fā)生了異常,父Actor可以通過預先設定的動作進行處理,處理方式有:恢復子Actor、重啟子Actor、停止子Actor、向上級Actor報告,這樣的處理方式被稱為“父監(jiān)督”模式。
圖3 Actor系統(tǒng)的樹形結構
協程并不是一個新概念,早在1963年協程這一概念就被提出[6-7],并在古老的Simula和Modula-2語言中得到了實現。但長期以來,協程這一概念并沒有引起足夠的重視,主流編程語言也鮮見對于協程的支持。隨著云計算、大數據時代的到來,如何提高軟件的并發(fā)能力以充分利用硬件性能成為重要的研究課題。在高并發(fā)條件下,協程具有上下文切換開銷小和資源利用率高的特性而重新得到開發(fā)人員的重視。隨著Lua、Golang、Kotlin等主流語言對于協程的支持越來越完善,協程重新登上歷史舞臺。
協程又稱纖程,是一種用戶級線程,操作系統(tǒng)不知道協程的存在。協程與進程或線程最直接的區(qū)別表現在,協程是編譯器層面的概念,而進程與線程則是操作系統(tǒng)層面的概念。協程能夠被掛起,稍后再在掛起的位置恢復執(zhí)行,其掛起和恢復是由開發(fā)者的程序邏輯自己控制的,通過主動掛起讓出運行權可以實現協程之間的協作。和進程與線程相比,協程具有以下幾個優(yōu)勢。其一,創(chuàng)建協程消耗的系統(tǒng)資源更小。協程的??臻g可以根據需要進行擴容和縮容,最小為內存頁長,而線程的棧空間大小一般為MB級別。其二,協程之間的切換代價更小。協程的調度發(fā)生在用戶態(tài),避免了線程上下文切換帶來的開銷。其三,協程可以實現無鎖編程。
2004年Lua語言之父Roberto Ierusalimschy在其發(fā)表的論文Revisiting Coroutines中,根據協程的實現方式的差異對協程進行了分類[8]。
按是否開辟調用棧,將協程分為有棧協程和無棧協程。有棧協程具有自己的調用棧,協程掛起時其中斷狀態(tài)會保存在調用棧中。其優(yōu)點是可以在任意函數調用層級的任意位置掛起,并轉移調度權。Lua語言中的協程是這種實現方式的典型代表。與之相對應,無棧協程沒有自己的調用棧,掛起點的狀態(tài)則是通過狀態(tài)機或者閉包等語法來實現。其優(yōu)點是內存開銷較小,Python語言中的Generator是這種協程的典型代表。
按調度方式的不同,將協程分為對稱協程和非對稱協程。對稱協程中每一個協程都是地位平等且相互獨立的,調度權可以在任意協程之間轉移。上文中提到的Go語言中的Goroutine是這種實現方式的典型代表,Goroutine可以通過對Channel的讀寫實現控制權的自由轉移。而非對稱協程在掛起后,只能將調度權出讓給它的調用者,即協程之間存在調用與被調用的關系。Lua語言中的協程是典型的非對稱協程,當前協程調用yield后總是將調度權讓給之前調用它的協程。
協程是用戶態(tài)的概念,其最終還是要運行在操作系統(tǒng)線程上,根據協程和線程的對應關系,可以分為多對一、一對一和多對多三類。多對一是指多個協程對應一個底層線程,只要內存足夠,一個線程中可以有任意多個協程,多個協程共享該線程分配到的計算機資源。其優(yōu)點是協程切換時不會進入內核態(tài),從而減少切換開銷。一對一是指協程和底層線程是一一對應的關系,這種方式可以充分利用多核性能,但是協程切換會進入內核態(tài),開銷較大。多對多是指協程和底層線程沒有特定對應關系。某一協程在某時刻可以在線程A執(zhí)行,一段時間之后又可能在線程B上執(zhí)行。這種方式融合了前兩種方式的優(yōu)點,但是實現較為困難。
Kotlin是一種運行在Java虛擬機上的靜態(tài)類型語言,由JetBrain公司設計開發(fā)。Kotlin目前是JVM語言家族中的重要成員之一,它的出現充分彌補了Java缺乏現代化編程語言特性的缺憾[9]。在Google I/O 2017大會上,Google宣布Kotlin成為Android官方開放語言。
Kotlin是少有的幾門從語法和標準庫兩個層面對協程提供支持的編程語言。在語法層面,被suspend關鍵字修飾的函數稱為掛起函數,Kotlin協程的掛起和恢復本質上就是掛起函數的掛起和恢復[10]。掛起函數只能在協程體內或其他掛起函數內調用,不能被普通函數調用。同時,除了標準庫以外,Kotlin官方還推出了面向生產環(huán)境的kotlin.coroutines框架,該框架提供了豐富的編程接口和組件來支撐生產環(huán)境中異步高并發(fā)程序的設計和實現,例如熱數據通道Channel、冷數據流Flow等高級數據結構。
Kotlin語言除了可以編譯為字節(jié)碼運行在Java虛擬機上以外,還可以通過LLVM編譯鏈工具最終編譯成機器碼,這一多平臺特性大大擴展了協程的應用場景。目前Kotlin對于協程的支持正在不斷完善,快速迭代,相信不久的將來,必將給開發(fā)者帶來開發(fā)體驗和開發(fā)效率上的全方位提升。
如表1所示,協程、CSP模型和Actor模型與傳統(tǒng)的多進程/多線程相比,具有通信方式靈活、并發(fā)度高、調用棧較小和開銷低的特點。
表1 并發(fā)編程模型之間的比較
進程、線程和協程的異同點表現在以下幾個方面:每個進程擁有獨立的堆和棧,進程之間既不共享堆,也不共享棧,其調度由操作系統(tǒng)負責。每個線程擁有獨立的棧和共享的堆,線程之間不共享棧,但同一進程內的線程共享堆空間,其調度亦由操作系統(tǒng)負責。協程,也是共享堆,不共享棧,但協程不是操作系統(tǒng)調度單元,而是由用戶調度。
CSP模式和Actor模式有許多共同點,兩者都通過消息傳遞的方式來避免共享內存引發(fā)的數據競爭問題,Actor模型中的Mailbox與CSP模型中的Channel都滿足先進先出的特性。但CSP與Actor有以下幾點比較大的區(qū)別:
(1)Actor模型中的Mailbox對程序員是透明的,Mailbox明確歸屬于某一個特定的Actor,是Actor模型的內部機制。CSP模型中的Channel對于程序員來說是可見的,必須由程序員手動創(chuàng)建。
(2)Actor模型中發(fā)送消息是非阻塞的。相反,CSP中的Channel是一個阻塞隊列,當Channel已滿時,繼續(xù)向Channel發(fā)送數據,會導致發(fā)送消息的Goroutine被掛起。當Channel為空時,繼續(xù)從Channel讀取數據,會導致接收消息的Goroutine被掛起。
(3)Actor模型理論上不保證消息百分比送達,而Go實現的CSP模型中,能保證消息百分百送達。
(4)Actor模型中,Actor與Mailbox是一對一的關系,每個Actor有且只有一個Mailbox,而在CSP中Channel和Process之間沒有從屬關系,兩者之間是多對多的關系,Process可以訂閱任意個Channel,一個Channel也可以被多個Process操作。
隨著多核CPU的高速發(fā)展和大規(guī)模分布式應用的逐漸普及,提高軟件系統(tǒng)的并發(fā)能力是當前亟待解決的問題,傳統(tǒng)的多進程和多線程編程方式無法較好地滿足快速構建高可用、高性能和高并發(fā)的分布式應用的要求。在當今互聯網高并發(fā)場景下,迫切需要新的并發(fā)編程方式來滿足高并發(fā)的需要,同時降低并發(fā)程序的開發(fā)難度。使用傳統(tǒng)并發(fā)編程方式寫出一個高性能并且可擴展的并發(fā)程序是相當困難的。高并發(fā)程序設計既是一個重點,也是一個難點。
提高系統(tǒng)的并發(fā)能力主要有垂直擴展和水平擴展兩種方式,垂直擴展通過提升單機性能來提高系統(tǒng)的并發(fā)性能,水平擴展則通過增加服務器數量提升并發(fā)性。未來分布式高并發(fā)應用場景的不斷多樣化,提高系統(tǒng)的并發(fā)性必將朝著垂直擴展和水平擴展相結合的方向發(fā)展。CSP、Actor和協程等并發(fā)計算模型是進行垂直擴展,提高系統(tǒng)并發(fā)能力的重要手段,隨著編程語言對并發(fā)計算模型的支持不斷完善,新的編程方式必將大大提升應用開發(fā)效率。