Python雖然也支持協程模式,但一直被詬病併發很弱,因爲GIL的關係,Python在單進程運行的情況下,無法利用多核CPU,所以就被其他能在單進程的情況下利用多核CPU的語言“吊打”性能了,相對而言Python語言是比較慢的。
Java常用的hotspot的JVM,採用的是第一種1:1的線程模型,即:map a java thread to a native thread,也就是說java線程會和native線程有個一一映射的關係,如果看下java的Thread類就可以發現有很多的native方法,這就涉及到操作系統的線程了。java因爲採用的是1:1的線程模型,線程數量特別是併發線程數會受到CPU和操作系統的限制。
Go
- Go是一門號稱從語言層面支持併發的編程語言,支持併發也是Go非常重要的特性之一
- Go支持協程,協程可以類比Java中的線程,解決併發問題的難點在於線程(協程)之間的協作
- Go提供了兩種方案
- 支持協程之間以共享內存的方式通信,Go提供了管程和原子類來對協程進行同步控制,該方案與Java類似
- 支持協程之間以消息傳遞的方式通信,本質上是要避免共享,該方案是基於CSP模型實現的,Go推薦該方案
CSP模型
- CSP:Communicating Sequential Processes
- Do not communicate by sharing memory; instead, share memory by communicating.
累加器
package main
import (
"fmt"
"time"
)
func main() {
singleCoroutine()
multiCoroutine()
}
// 單協程,只能用到CPU的一個核
func singleCoroutine() {
var result, i uint64
start := time.Now()
for i = 1; i <= 10000000000; i++ {
result += i
}
elapsed := time.Since(start)
fmt.Println(elapsed, result) // 4.330357206s 13106511857580896768
}
// 多協程
func multiCoroutine() {
var result uint64
start := time.Now()
ch1 := calc(1, 2500000000)
ch2 := calc(2500000001, 5000000000)
ch3 := calc(5000000001, 7500000000)
ch4 := calc(7500000001, 10000000000)
// 主協程需要與子協程通信,Go中協程之間的通信推薦使用channel
result = <-ch1 + <-ch2 + <-ch3 + <-ch4
// ch1只能讀取數據,如果通過ch1寫入數據,編譯時會報錯
// ch1 <- 7 // invalid operation: ch1 <- 7 (send to receive-only type <-chan uint64)
elapsed := time.Since(start)
fmt.Println(elapsed, result) // 1.830920702s 13106511857580896768
}
// 返回一個只能接收數據的channel
// 方法創建的子協程會把計算結果發送到這個channel,而主協程會通過channel把計算結果取出來
func calc(from uint64, to uint64) <-chan uint64 {
// channel用於協程間的通信,這是一個無緩衝的channel
channel := make(chan uint64)
go func() {
result := from
for i := from + 1; i <= to; i++ {
result += i
}
// 將結果寫入channel
channel <- result
}()
// 返回用於通信的channel
return channel
}
生產者-消費者模式
- 可以把Go實現的CSP模式類比成生產者-消費者模式,而channel類比成生產者-消費者模式中的阻塞隊列
- Go中channel的容量可以爲0,容量爲0的channel被稱爲無緩衝的channel,容量大於0的channel被稱爲有緩衝的channel
- 無緩衝的channel類似於Java中提供的SynchronousQueue,主要用途是在兩個協程之間做數據交換
- Go中的channel是語言層面支持的,使用左向箭頭<-完成向channel發送數據和讀取數據的任務
- Go中的channel是支持雙向傳輸的,即一個協程既可以通過它發送數據,也可以通過它接收數據
- Go中的雙向channel可以變成一個單向channel
- calc中創建了一個雙向channel,但是返回的是一個只能接收數據的單向channel
- 所以在主協程中,只能通過該channel接收數據,而不能通過它發送數據
// 創建一個容量爲4的channel
channel := make(chan int, 4)
// 創建4個協程,作爲生產者
for i := 0; i < 4; i++ {
go func() {
channel <- 7
}()
}
// 創建4個協程,作爲消費者
for i := 0; i < 4; i++ {
go func() {
o := <-channel
fmt.Println("received : ", o)
}()
}
Actor模式
- Go實現的CSP模式和Actor模式都是通過消息傳遞的方式來避免共享,主要有以下三個區別
- Actor模型中沒有channel,Actor模型中的Mailbox與channel非常類似,看起來都是FIFO隊列,但本質區別很大
- Actor模型
- Mailbox對程序員是透明的,Mailbox明確歸屬於某一個特定的Actor,是Actor模型的內部機制
- Actor之間可以直接通信,不需要通信媒介
- CSP模型
- channel對於程序員來說是可見的
- channel是通信媒介,傳遞的消息都直接發送到channel中
- Actor模型中發送消息是非阻塞的,而CSP模型中是阻塞的
- Go實現的CSP模型,channel是一個阻塞隊列
- 當阻塞隊列已滿的時候,向channel發送數據,會導致發送消息的協程阻塞
- Actor模型理論上不保證消息百分比送達,而Go實現的CSP模型中,是能保證消息百分百送達的(代價:可能導致死鎖)
func main() {
// 無緩衝的channel
channel := make(chan int)
// fatal error: all goroutines are asleep - deadlock!
// 主協程會阻塞在此處,發生死鎖
<-channel
}
小結
- CSP模型是Tony Hoare在1978年提出的,該模型一直都在發展,其理論遠比Go實現的複雜得多
- Tony Hoare在併發領域還有另一項重要成就,即霍爾管程模型,這是Java解決併發問題的理論基礎
- Java可以藉助第三方類庫JCSP來支持CSP模型,相比Go的實現,JCSP更接近理論模型
- JCSP並沒有經過廣泛的生產環境檢驗,因此不推薦在生產環境使用