1. 程式人生 > 其它 >1.3 Go語言為併發而生

1.3 Go語言為併發而生

技術標籤:Golanggogo語言程式語言golang

  在早期 CPU 都是以單核的形式順序執行機器指令。Go語言的祖先C語言正是這種順序程式語言的代表。順序程式語言中的順序是指:所有的指令都是以序列的方式執行,在相同的時刻有且僅有一個 CPU 在順序執行程式的指令。

  隨著處理器技術的發展,單核時代以提升處理器頻率來提高執行效率的方式遇到了瓶頸,單核 CPU 發展的停滯,給多核 CPU 的發展帶來了機遇。相應地,程式語言也開始逐步向並行化的方向發展。

  雖然一些程式語言的框架在不斷地提高多核資源使用效率,例如 Java 的 Netty 等,但仍然需要開發人員花費大量的時間和精力搞懂這些框架的執行原理後才能熟練掌握。

  作為程式設計師,要開發出能充分利用硬體資源的應用程式是一件很難的事情。現代計算機都擁有多個核,但是大部分程式語言都沒有有效的工具讓程式可以輕易利用這些資源。程式設計時需要寫大量的執行緒同步程式碼來利用多個核,很容易導致錯誤。

  Go語言正是在多核和網路化的時代背景下誕生的原生支援併發的程式語言。Go語言從底層原生支援併發,無須第三方庫,開發人員可以很輕鬆地在編寫程式時決定怎麼使用 CPU 資源。

  Go語言的併發是基於 goroutine 的,goroutine 類似於執行緒,但並非執行緒。可以將 goroutine 理解為一種虛擬執行緒。Go語言執行時會參與排程 goroutine,並將 goroutine 合理地分配到每個 CPU 中,最大限度地使用 CPU 效能。

  多個 goroutine 中,Go語言使用通道(channel)進行通訊,通道是一種內建的資料結構,可以讓使用者在不同的 goroutine 之間同步傳送具有型別的訊息。這讓程式設計模型更傾向於在 goroutine 之間傳送訊息,而不是讓多個 goroutine 爭奪同一個資料的使用權。

  程式可以將需要併發的環節設計為生產者模式和消費者的模式,將資料放入通道。通道另外一端的程式碼將這些資料進行併發計算並返回結果,如下圖所示。
在這裡插入圖片描述

提示:Go語言通過通道可以實現多個 goroutine 之間記憶體共享。

【例項】生產者每秒生成一個字串,並通過通道傳給消費者,生產者使用兩個 goroutine 併發執行,消費者在 main() 函式的 goroutine 中進行處理。

package main

import (
        "fmt"
        "math/rand"
        "time"
)

// 資料生產者
func producer(header string, channel chan<- string) {
     // 無限迴圈, 不停地生產資料
     for {
            // 將隨機數和字串格式化為字串傳送給通道
            channel <- fmt.Sprintf("%s: %v", header, rand.Int31())
            // 等待1秒
            time.Sleep(time.Second)
        }
}

// 資料消費者
func customer(channel <-chan string) {
     // 不停地獲取資料
     for {
            // 從通道中取出資料, 此處會阻塞直到通道中返回資料
            message := <-channel
            // 列印資料
            fmt.Println(message)
        }
}

func main() {
    // 建立一個字串型別的通道
    channel := make(chan string)
    // 建立producer()函式的併發goroutine
    go producer("cat", channel)
    go producer("dog", channel)
    // 資料消費函式
    customer(channel)
}

執行結果:

dog: 2019727887
cat: 1298498081
dog: 939984059
cat: 1427131847
cat: 911902081
dog: 1474941318
dog: 140954425
cat: 336122540
cat: 208240456
dog: 646203300
dog: 2019727887
cat: 1298498081
dog: 939984059
cat: 1427131847
cat: 911902081
dog: 1474941318
dog: 140954425
cat: 336122540
cat: 208240456
dog: 646203300

對程式碼的分析:

  • 第 03 行,匯入格式化(fmt)、隨機數(math/rand)、時間(time)包參與編譯。
  • 第 10 行,生產資料的函式,傳入一個標記型別的字串及一個只能寫入的通道。
  • 第 13 行,for{} 構成一個無限迴圈。
  • 第 15 行,使用 rand.Int31() 生成一個隨機數,使用 fmt.Sprintf() 函式將 header
    和隨機數格式化為字串。
  • 第 18 行,使用 time.Sleep() 函式暫停 1 秒再執行這個函式。如果在 goroutine 中執行時,暫停不會影響其他goroutine 的執行。
  • 第 23 行,消費資料的函式,傳入一個只能寫入的通道。
  • 第 26 行,構造一個不斷消費訊息的迴圈。
  • 第 28 行,從通道中取出資料。
  • 第 31 行,將取出的資料進行列印。
  • 第 35 行,程式的入口函式,總是在程式開始時執行。
  • 第 37 行,例項化一個字串型別的通道。
  • 第 39 行和第 40 行,併發執行一個生產者函式,兩行分別建立了這個函式搭配不同引數的兩個 goroutine。
  • 第 42 行,執行消費者函式通過通道進行資料消費。

  整段程式碼中,沒有執行緒建立,沒有執行緒池也沒有加鎖,僅僅通過關鍵字 go 實現 goroutine,和通道實現資料交換。