Golang 爲什麼相對Python、Java速度更快更高效

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

  1. Go是一門號稱從語言層面支持併發的編程語言,支持併發也是Go非常重要的特性之一
  2. Go支持協程,協程可以類比Java中的線程,解決併發問題的難點在於線程(協程)之間的協作
  3. Go提供了兩種方案
    • 支持協程之間以共享內存的方式通信,Go提供了管程和原子類來對協程進行同步控制,該方案與Java類似
    • 支持協程之間以消息傳遞的方式通信,本質上是要避免共享,該方案是基於CSP模型實現的,Go推薦該方案

CSP模型

  1. CSP:Communicating Sequential Processes
  2. 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
}

生產者-消費者模式

  1. 可以把Go實現的CSP模式類比成生產者-消費者模式,而channel類比成生產者-消費者模式中的阻塞隊列
  2. Go中channel的容量可以爲0,容量爲0的channel被稱爲無緩衝的channel,容量大於0的channel被稱爲有緩衝的channel
  3. 無緩衝的channel類似於Java中提供的SynchronousQueue,主要用途是在兩個協程之間做數據交換
  4. Go中的channel是語言層面支持的,使用左向箭頭<-完成向channel發送數據和讀取數據的任務
  5. Go中的channel是支持雙向傳輸的,即一個協程既可以通過它發送數據,也可以通過它接收數據
  6. 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模式

  1. Go實現的CSP模式和Actor模式都是通過消息傳遞的方式來避免共享,主要有以下三個區別
  2. Actor模型中沒有channel,Actor模型中的Mailbox與channel非常類似,看起來都是FIFO隊列,但本質區別很大
  • Actor模型
    • Mailbox對程序員是透明的,Mailbox明確歸屬於某一個特定的Actor,是Actor模型的內部機制
    • Actor之間可以直接通信,不需要通信媒介
  • CSP模型
    • channel對於程序員來說是可見的
    • channel是通信媒介,傳遞的消息都直接發送到channel中
  1. Actor模型中發送消息是非阻塞的,而CSP模型中是阻塞的
  • Go實現的CSP模型,channel是一個阻塞隊列
  • 當阻塞隊列已滿的時候,向channel發送數據,會導致發送消息的協程阻塞
  1. Actor模型理論上不保證消息百分比送達,而Go實現的CSP模型中,是能保證消息百分百送達的(代價:可能導致死鎖)

 

func main() {
    // 無緩衝的channel
    channel := make(chan int)
    // fatal error: all goroutines are asleep - deadlock!
    // 主協程會阻塞在此處,發生死鎖
    <-channel
}

小結

  1. CSP模型是Tony Hoare在1978年提出的,該模型一直都在發展,其理論遠比Go實現的複雜得多
    • Tony Hoare在併發領域還有另一項重要成就,即霍爾管程模型,這是Java解決併發問題的理論基礎
  2. Java可以藉助第三方類庫JCSP來支持CSP模型,相比Go的實現,JCSP更接近理論模型
    • JCSP並沒有經過廣泛的生產環境檢驗,因此不推薦在生產環境使用

 

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