瞅一眼就會使用GO的併發程式設計分享

小魔童哪吒發表於2021-06-11
[TOC]

之前我們分享了網路程式設計,今天我們來看看GO的併發程式設計分享,我們先來看看他是個啥

啥是併發程式設計呢?

指在一臺處理器上同時處理多個任務

此處說的同時,可不是同一個時間一起手拉手做同一件事情

併發是在同一實體上的多個事件,而這個事件在同一時間間隔發生的,同一個時間段,有多個任務執行,可是同一個時間點,只有一個任務在執行

為啥要有併發程式設計?

隨著網際網路的普及,網際網路使用者人數原來越多,這對系統的效能帶來了巨大的挑戰。

我們要通過各種方式來高效利用硬體的效能(壓榨),從而提高系統的效能進而提升使用者體驗,提升團隊或者企業的競爭力。

併發是為了解決什麼問題?目的是啥?

充分的利用好處理器的每一個核,以達到最高的處理效能,儘可能的運用好每一塊磚

可是由於現在我們使用的CPU,記憶體,IO三者之間速度不盡相同

我們為了提高系統效能,計算機系統會將這三者速度進行平衡,以達到最優的效果,都有如下措施:

  • 作業系統增加了程式執行緒,以分時複用 CPU,進而均衡 CPUI/O 裝置的速度差異;
  • CPU 增加了快取,以均衡與記憶體的速度差異;
  • 編譯程式優化指令執行次序,使得快取能夠得到更加合理地利用。

說到程式和執行緒,他們都是幹啥的呢,我們們順帶說一下?

  • 程式是程式在作業系統中的一次執行過程

是 系統進行資源分配和排程的一個獨立單位

  • 執行緒是程式的一個執行實體

是 CPU 排程和分派的基本單位,它是比程式更小的能獨立執行的基本單位。

  • 一個程式可以建立和撤銷多個執行緒, 並且同一個程式中的多個執行緒之間可以併發執行。

講到併發程式設計不得不說併發和並行有啥區別?是不是總是有小夥伴弄不清楚他們到底是啥區別,好像一樣,又好像不一樣

併發和並行的區別

一言蔽之,區別如下:

併發

多執行緒程式在一個核的 CPU 上執行

並行

多執行緒程式在多個核的 CPU 上執行

併發就像多個小夥伴跑接力,同一個時間點只會有一個小夥伴在跑,互相有影響

並行就像是多個小夥伴同一個起點一起跑,互不干擾

我們需要記住一點,再強調一波:

併發不是並行

併發主要由切換時間片來實現”同時”執行

並行則是直接利用多核實現多執行緒的執行,

在 GO 可以設定使用核數,以發揮多核計算機的能力,不過設定核數都是依賴於硬體的

那麼,講到GO的併發程式設計,就必須上我們的主角,那就是協程

協程 goroutine 是啥?

協程是一種程式元件

是由子例程(過程、函式、例程、方法、子程式)的概念泛化而來的

子例程只有一個入口點且只返回一次,而協程允許多個入口點,可以在指定位置掛起和恢復執行。

協程和執行緒分別有啥特點嘞

  • 協程

獨立的棧空間,共享堆空間,排程由使用者自己控制

本質上有點類似於使用者級執行緒,這些使用者級執行緒的排程也是自己實現的。

  • 執行緒

一個執行緒上可以跑多個協程,協程是輕量級的執行緒

GO 高併發的原因是啥?

  • goroutine 奉行通過通訊來共享記憶體
  • 每個一個GO的例項有4~5KB的棧記憶體佔用,並且由於 GO 實現機制而大幅減少的建立和銷燬開銷
  • Golang 在語言層面上就支援協程 goroutine

GOLANG併發程式設計涉及哪些知識點呢?

  • 基本協程的原理,實現方式,雖然說,GO中使用協程很方便,可以我們必須要知其然而知其所以然
  • Goroutine 池
  • runtime 包的使用
  • Channel 通道
  • 定時器
  • 併發且安全的鎖
  • 原子操作
  • select 多路複用
  • 等等…

Goroutine的那些事

我們寫C/C++的時候,我們必然也是要實現併發程式設計

我們通常需要自己維護一個執行緒池,並且需要自己去包裝一個又一個的任務,同時需要自己去排程執行緒執行任務並維護上下文切換

且做執行緒池的時候,我們需要自己做一個執行緒管理的角色,靈活動態壓縮和擴容

可是能不能有這樣一種機制,我們只需要定義多個任務,讓系統去幫助我們把這些任務分配到CPU上實現併發執行

GO裡面就正好有這樣的機制

goroutine 的概念類似於執行緒

goroutine 是由Go的執行時(runtime)排程和管理的

Go程式會智慧地將 goroutine 中的任務合理地分配給每個CPU

Go 在語言層面已經內建了排程和上下文切換的機制

寫 GO 比較爽的一個地方是:

在GO裡面,你不需要去自己寫程式、執行緒、協程

我們可以使用 goroutine 包

如何使用 goroutine ?

我們需要讓某個任務併發執行的時候,只需要把這個任務包裝成一個函式

專門開啟一個 goroutine 協程 去執行這個函式就可以了 , GO一個協程,很方便

一個 goroutine 必定對應一個函式,可以建立多個 goroutine 去執行相同的函式,只是多個協程都是做同一個事情罷了

我們先來使用一下協程,再來拋磚引玉,適當的分享一下

啟動單個協程

func Hi() {
    fmt.Println("this is Hi Goroutine!")
}
func main() {
    Hi()
    fmt.Println("main goroutine!")
}

我們一般呼叫函式是如上這個樣子的,效果如下

this is Hi Goroutine!
main goroutine!

其實我們呼叫協程的話,也與上述類似

我們可以使用 go 後面加上函式名字,來開闢一個協程,專門做函式需要執行的事情

func main() {
    go Hi() // 啟動一個goroutine 協程 去執行 Hi 函式
    fmt.Println("main goroutine!")

實際效果我們可以看到,程式只列印了 main goroutine!

main goroutine!

在程式啟動的時候,Go 程式就會為 main() 函式建立一個預設的 goroutine 協程

當 main() 函式返回的時候,剛開闢的另外一個 goroutine 協程 就結束了

所有在 main() 函式中啟動的 goroutine 協程 會一同結束,老大死了,其餘的傀儡也灰飛煙滅了

img

我們也可以讓主協程等等一定子協程,待子協程處理完自己的事情,退出後,主協程再自己退出,這和我們寫C/C++程式 和 執行緒的時候,類似

簡單的,我們可以使用 time.sleep 函式來讓主協程阻塞等待

我們也可以使用 上述提到的 使用 select{} 來達到目的

當然也有其他的方式,後續文章會慢慢的分享到

多個協程

那麼多個協程又是怎麼玩的呢?

我們使用 sync.WaitGroup 來實現goroutine 協程的同步

package main

import (
    "fmt"
    "sync"
)

var myWg sync.WaitGroup

func Hi(i int) {
    // goroutine 協程 結束就 記錄 -1
    defer myWg.Done()
    fmt.Println("Hello Goroutine! the ", i)
}
func main() {

    for i := 0; i < 10; i++ {
        // 啟動一個goroutine 協程 就記錄 +1
        myWg.Add(1)
        go Hi(i)
    }

    // 等待所有記錄 的goroutine 協程 都結束
    myWg.Wait() 
}

會有如下輸出,每一個協程列印的數字並不是按照順序來的:

Hello Goroutine! the  9
Hello Goroutine! the  4
Hello Goroutine! the  2
Hello Goroutine! the  3
Hello Goroutine! the  6
Hello Goroutine! the  5
Hello Goroutine! the  7
Hello Goroutine! the  8
Hello Goroutine! the  1
Hello Goroutine! the  0

還是同樣的, 如果是主協程先退出,那麼子協程還行繼續執行嗎?

毋庸置疑,主協程退出,子協程也會跟著退出

GO 中的 協程

分享如下幾個點

GO中的棧是可增長的

一般都有固定的棧記憶體(通常為2MB),goroutine 的棧不是固定的,goroutine 的棧大小可以擴充套件到1GB

goroutine 是如何排程

這就不得不提 GPM

GPM是Go語言執行時(runtime)層面實現的,我們先簡單瞭解一下GPM分別代表啥

G

就是個 goroutine ,裡面除了存放本 goroutine 資訊外 還有與所在P的繫結等資訊

P

Processor 管理著一組 goroutine 佇列

P 裡面會儲存當前 goroutine 執行的上下文環境(函式指標,堆疊地址及地址邊界)

P 會對自己管理的 goroutine 佇列做一些排程(比如把佔用CPU時間較長的 goroutine 暫停、執行後續的 goroutine)

當自己的佇列消費完了就去全域性佇列裡取,如果全域性佇列裡也消費完了會去其他P的佇列裡搶任務。

M(machine)

是 Go 執行時(runtime)對作業系統核心執行緒的虛擬

M 與核心執行緒一般是一一對映的關係, 一個 groutine 最終是要放到 M上執行

這裡的 P 與 M 一般也是一一對應的

P 管理著一組G 掛載在 M 上執行

當一個 G 長久阻塞在一個 M 上時,runtime 會新建一個M,

阻塞 G 所在的 P 會把其他的 G 掛載在新建的M上

這個時候,當舊的 G 阻塞完成或者認為其已經掛了的話,就會回收舊的 M

還有一點

P 的個數是通過 runtime.GOMAXPROCS 設定(最大256),這個數字也依賴於自己的硬體,在併發量大的時候會增加一些 P 和 M ,但不會太多

總結

  • 分享了併發和並行
  • 分享了GO 的併發,協程的簡單使用
  • 簡單分享了GO可伸縮擴充套件的棧記憶體

關於GO的併發程式設計知識點涉及面多,對於他的排程原理,真實感興趣的話,可以看上述提到的GO併發涉及的知識點,一點一點的深入下去,今天就到這裡,大體瞭解GO協程的使用

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,下一次GO的鎖和原子操作分享

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章