之前我們分享了網路程式設計,今天我們來看看GO的併發程式設計分享,我們先來看看他是個啥
啥是併發程式設計呢?
指在一臺處理器上同時處理多個任務
此處說的同時,可不是同一個時間一起手拉手做同一件事情
併發是在同一實體上的多個事件,而這個事件在同一時間間隔發生的,同一個時間段,有多個任務執行,可是同一個時間點,只有一個任務在執行
為啥要有併發程式設計?
隨著網際網路的普及,網際網路使用者人數原來越多,這對系統的效能帶來了巨大的挑戰。
我們要通過各種方式來高效利用硬體的效能(壓榨),從而提高系統的效能進而提升使用者體驗,提升團隊或者企業的競爭力。
併發是為了解決什麼問題?目的是啥?
是充分的利用好處理器的每一個核,以達到最高的處理效能,儘可能的運用好每一塊磚
可是由於現在我們使用的CPU,記憶體,IO三者之間速度不盡相同
我們為了提高系統效能,計算機系統會將這三者速度進行平衡,以達到最優的效果,都有如下措施:
- 作業系統增加了程式、執行緒,以分時複用 CPU,進而均衡 CPU 與 I/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 協程 會一同結束,老大死了,其餘的傀儡也灰飛煙滅了
我們也可以讓主協程等等一定子協程,待子協程處理完自己的事情,退出後,主協程再自己退出,這和我們寫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 協議》,轉載必須註明作者和本文連結