Java并发编程实战~协程
Golang 是一門號稱從語言層面支持并發的編程語言,支持并發是 Golang 一個非常重要的特性。在上一篇文章《44 | 協程:更輕量級的線程》中我們介紹過,Golang 支持協程,協程可以類比 Java 中的線程,解決并發問題的難點就在于線程(協程)之間的協作。
那 Golang 是如何解決協作問題的呢?
總的來說,Golang 提供了兩種不同的方案:一種方案支持協程之間以共享內存的方式通信,Golang 提供了管程和原子類來對協程進行同步控制,這個方案與 Java 語言類似;另一種方案支持協程之間以消息傳遞(Message-Passing)的方式通信,本質上是要避免共享,Golang 的這個方案是基于 CSP(Communicating Sequential Processes)模型實現的。Golang 比較推薦的方案是后者。
什么是 CSP 模型
我們在《42 | Actor 模型:面向對象原生的并發模型》中介紹了 Actor 模型,Actor 模型中 Actor 之間就是不能共享內存的,彼此之間通信只能依靠消息傳遞的方式。Golang 實現的 CSP 模型和 Actor 模型看上去非常相似,Golang 程序員中有句格言:“不要以共享內存方式通信,要以通信方式共享內存(Don’t communicate by sharing memory, share memory by communicating)。”雖然 Golang 中協程之間,也能夠以共享內存的方式通信,但是并不推薦;而推薦的以通信的方式共享內存,實際上指的就是協程之間以消息傳遞方式來通信。
下面我們先結合一個簡單的示例,看看 Golang 中協程之間是如何以消息傳遞的方式實現通信的。我們示例的目標是打印從 1 累加到 100 億的結果,如果使用單個協程來計算,大概需要 4 秒多的時間。單個協程,只能用到 CPU 中的一個核,為了提高計算性能,我們可以用多個協程來并行計算,這樣就能發揮多核的優勢了。
在下面的示例代碼中,我們用了 4 個子協程來并行執行,這 4 個子協程分別計算[1, 25 億]、(25 億, 50 億]、(50 億, 75 億]、(75 億, 100 億],最后再在主協程中匯總 4 個子協程的計算結果。主協程要匯總 4 個子協程的計算結果,勢必要和 4 個子協程之間通信,Golang 中協程之間通信推薦的是使用 channel,channel 你可以形象地理解為現實世界里的管道。另外,calc() 方法的返回值是一個只能接收數據的 channel ch,它創建的子協程會把計算結果發送到這個 ch 中,而主協程也會將這個計算結果通過 ch 讀取出來。
import ("fmt""time" ) func main() {// 變量聲明var result, i uint64// 單個協程執行累加操作start := time.Now()for i = 1; i <= 10000000000; i++ {result += i}// 統計計算耗時elapsed := time.Since(start)fmt.Printf("執行消耗的時間為:", elapsed)fmt.Println(", result:", result)// 4個協程共同執行累加操作start = time.Now()ch1 := calc(1, 2500000000)ch2 := calc(2500000001, 5000000000)ch3 := calc(5000000001, 7500000000)ch4 := calc(7500000001, 10000000000)// 匯總4個協程的累加結果result = <-ch1 + <-ch2 + <-ch3 + <-ch4// 統計計算耗時elapsed = time.Since(start)fmt.Printf("執行消耗的時間為:", elapsed)fmt.Println(", result:", result) } // 在協程中異步執行累加操作,累加結果通過channel傳遞 func calc(from uint64, to uint64) <-chan uint64 {// channel用于協程間的通信ch := make(chan uint64)// 在協程中執行累加操作go func() {result := fromfor i := from + 1; i <= to; i++ {result += i}// 將結果寫入channelch <- result}()// 返回結果是用于通信的channelreturn ch }CSP 模型與生產者 - 消費者模式
你可以簡單地把 Golang 實現的 CSP 模型類比為生產者 - 消費者模式,而 channel 可以類比為生產者 - 消費者模式中的阻塞隊列。不過,需要注意的是 Golang 中 channel 的容量可以是 0,容量為 0 的 channel 在 Golang 中被稱為無緩沖的 channel,容量大于 0 的則被稱為有緩沖的 channel。
無緩沖的 channel 類似于 Java 中提供的 SynchronousQueue,主要用途是在兩個協程之間做數據交換。比如上面累加器的示例代碼中,calc() 方法內部創建的 channel 就是無緩沖的 channel。
而創建一個有緩沖的 channel 也很簡單,在下面的示例代碼中,我們創建了一個容量為 4 的 channel,同時創建了 4 個協程作為生產者、4 個協程作為消費者。
Golang 中的 channel 是語言層面支持的,所以可以使用一個左向箭頭(<-)來完成向 channel 發送數據和讀取數據的任務,使用上還是比較簡單的。Golang 中的 channel 是支持雙向傳輸的,所謂雙向傳輸,指的是一個協程既可以通過它發送數據,也可以通過它接收數據。
不僅如此,Golang 中還可以將一個雙向的 channel 變成一個單向的 channel,在累加器的例子中,calc() 方法中創建了一個雙向 channel,但是返回的就是一個只能接收數據的單向 channel,所以主協程中只能通過它接收數據,而不能通過它發送數據,如果試圖通過它發送數據,編譯器會提示錯誤。對比之下,雙向變單向的功能,如果以 SDK 方式實現,還是很困難的。
CSP 模型與 Actor 模型的區別
同樣是以消息傳遞的方式來避免共享,那 Golang 實現的 CSP 模型和 Actor 模型有什么區別呢?
第一個最明顯的區別就是:Actor 模型中沒有 channel。雖然 Actor 模型中的 mailbox 和 channel 非常像,看上去都像個 FIFO 隊列,但是區別還是很大的。Actor 模型中的 mailbox 對于程序員來說是“透明”的,mailbox 明確歸屬于一個特定的 Actor,是 Actor 模型中的內部機制;而且 Actor 之間是可以直接通信的,不需要通信中介。但 CSP 模型中的 channel 就不一樣了,它對于程序員來說是“可見”的,是通信的中介,傳遞的消息都是直接發送到 channel 中的。
第二個區別是:Actor 模型中發送消息是非阻塞的,而 CSP 模型中是阻塞的。Golang 實現的 CSP 模型,channel 是一個阻塞隊列,當阻塞隊列已滿的時候,向 channel 中發送數據,會導致發送消息的協程阻塞。
第三個區別則是關于消息送達的。在《42 | Actor 模型:面向對象原生的并發模型》這篇文章中,我們介紹過 Actor 模型理論上不保證消息百分百送達,而在 Golang 實現的 CSP 模型中,是能保證消息百分百送達的。不過這種百分百送達也是有代價的,那就是有可能會導致死鎖。
比如,下面這段代碼就存在死鎖問題,在主協程中,我們創建了一個無緩沖的 channel ch,然后從 ch 中接收數據,此時主協程阻塞,main() 方法中的主協程阻塞,整個應用就阻塞了。這就是 Golang 中最簡單的一種死鎖。
總結
Golang 中雖然也支持傳統的共享內存的協程間通信方式,但是推薦的還是使用 CSP 模型,以通信的方式共享內存。
Golang 中實現的 CSP 模型功能上還是很豐富的,例如支持 select 語句,select 語句類似于網絡編程里的多路復用函數 select(),只要有一個 channel 能夠發送成功或者接收到數據就可以跳出阻塞狀態。鑒于篇幅原因,我就點到這里,不詳細介紹那么多了。
CSP 模型是托尼·霍爾(Tony Hoare)在 1978 年提出的,不過這個模型這些年一直都在發展,其理論遠比 Golang 的實現復雜得多,如果你感興趣,可以參考霍爾寫的Communicating Sequential Processes這本電子書。另外,霍爾在并發領域還有一項重要成就,那就是提出了霍爾管程模型,這個你應該很熟悉了,Java 領域解決并發問題的理論基礎就是它。
Java 領域可以借助第三方的類庫JCSP來支持 CSP 模型,相比 Golang 的實現,JCSP 更接近理論模型,如果你感興趣,可以下載學習。不過需要注意的是,JCSP 并沒有經過廣泛的生產環境檢驗,所以并不建議你在生產環境中使用。
?
總結
以上是生活随笔為你收集整理的Java并发编程实战~协程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ActiveMQ消费者平滑关闭
- 下一篇: java美元兑换,(Java实现) 美元