golang 之并发
在了解之前,要注意golang是并發(fā)語言而不是并行語言
并發(fā)和并行
并發(fā)是一次性做大量事情的能力(兩個(gè)或多個(gè)事件在同一時(shí)間間隔發(fā)生)
并行同一時(shí)間執(zhí)行多個(gè)任務(wù)的能力(兩個(gè)或者多個(gè)事件在同一時(shí)刻發(fā)生)
舉例說明:
每天早上10分鐘我洗臉,刷牙,吃早飯等等很多事情,這就是并發(fā)。 我一邊刷牙的同時(shí)在燒水做飯這就是并行。
技術(shù)層面來說:假如一個(gè)web網(wǎng)頁中有視頻播放和文件下載兩個(gè)動作,當(dāng)瀏覽器在單核的處理器下運(yùn)行時(shí),CPU核心會在這兩個(gè)事件中來回切換,(同時(shí))播放視頻和下載,這就稱為并發(fā)。并發(fā)進(jìn)程在不同的時(shí)間點(diǎn)開始并有著重疊的執(zhí)行周期。假如你的CPU是多核處理器,那么下載和播放會在不同的CPU核心同時(shí)執(zhí)行,這就是并行。
goroutine
在go中,每一個(gè)并發(fā)執(zhí)行的操作都稱為goroutine,當(dāng)一個(gè)程序啟動時(shí),只有一個(gè)goroutine來調(diào)用main函數(shù),稱它為主goroutine。新的goroutine通過go語法來創(chuàng)建。
f() // 調(diào)用f(); 等待它返回 go f() //新建一個(gè)調(diào)用f()的goroutine,不用等待。
調(diào)度模型
groutine能擁有強(qiáng)大的并發(fā)實(shí)現(xiàn)是通過GPM調(diào)度模型實(shí)現(xiàn):
G:代表一個(gè)goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調(diào)度。
M: 代表內(nèi)核級線程,一個(gè)M就是一個(gè)線程,goroutine就是跑在M之上的;M是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對象內(nèi)存cache(mcache)、當(dāng)前執(zhí)行的goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息
P: 全程processor,處理器,它的主要作用來執(zhí)行g(shù)oroutine,所以它維護(hù)了一個(gè)goroutine隊(duì)列。里面存儲了所有需要它來執(zhí)行的goroutine。
Sched:代表調(diào)度器,它維護(hù)有存儲M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等。
調(diào)度實(shí)現(xiàn)
有2個(gè)物理線程M,每一個(gè)M都擁有一個(gè)處理器P,每一個(gè)也都有一個(gè)正在運(yùn)行的goroutine。
P的數(shù)量可以通過GOMAXPROCS()來設(shè)置,它其實(shí)也就代表了真正的并發(fā)度,即有多少個(gè)goroutine可以同時(shí)運(yùn)行。圖中灰色的那些goroutine并沒有運(yùn)行,而是出于ready的就緒態(tài),正在等待被調(diào)度。P維護(hù)著這個(gè)隊(duì)列(稱之為runqueue),
Go語言里,啟動一個(gè)goroutine很容易:go function 就行,所以每有一個(gè)go語句被執(zhí)行,runqueue隊(duì)列就在其末尾加入一個(gè)goroutine,在下一個(gè)調(diào)度點(diǎn),就從runqueue中取出(如何決定取哪個(gè)goroutine?)一個(gè)goroutine執(zhí)行。
如果一個(gè)線程阻塞會發(fā)生什么情況呢?如下圖
從上圖中可以看出,一個(gè)線程放棄了它的上下文讓其他的線程可以運(yùn)行它。M1可能僅僅為了讓它處理圖中系統(tǒng)調(diào)用而被創(chuàng)建出來,或者它可能來自一個(gè)線程池。這個(gè)處于系統(tǒng)調(diào)用中的線程將會保持在這個(gè)導(dǎo)致系統(tǒng)調(diào)用的goroutine上,因?yàn)閺募夹g(shù)上來說,它仍然在執(zhí)行,雖然阻塞在OS里了。
另一種情況是P所分配的任務(wù)G很快就執(zhí)行完了(分配不均),這就導(dǎo)致了這個(gè)處理器P很忙,但是其他的P還有任務(wù),此時(shí)如果global runqueue沒有任務(wù)G了,那么P不得不從其他的P里拿一些G來執(zhí)行。一般來說,如果P從其他的P那里要拿任務(wù)的話,一般就拿run queue的一半,這就確保了每個(gè)OS線程都能充分的使用,
使用goroutine
package main
import (
"fmt"
"time"
)
func cal(a int , b int ) {
c := a+b
fmt.Printf("%d + %d = %d
",a,b,c)
}
func main() {
for i :=0 ; i<10 ;i++{
go cal(i,i+1) //啟動10個(gè)goroutine 來計(jì)算
}
time.Sleep(time.Second * 2) // sleep作用是為了等待所有任務(wù)完成
}
GOMAXPROCS
設(shè)置goroutine運(yùn)行的CPU數(shù)量,最新版本的go已經(jīng)默認(rèn)已經(jīng)設(shè)置了。
num := runtime.NumCPU() //獲取主機(jī)的邏輯CPU個(gè)數(shù) runtime.GOMAXPROCS(num) //設(shè)置可同時(shí)執(zhí)行的最大CPU數(shù)
也可以根據(jù)個(gè)人手動設(shè)置,例如
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
上面GOMAXPROCS設(shè)置為1,當(dāng)遇到兩個(gè)go調(diào)度時(shí),就會發(fā)生等待。如果設(shè)置為2就會并行執(zhí)行(前提是你的cpu數(shù)量>=2),如下例子
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
在執(zhí)行上面代碼時(shí)細(xì)心的同學(xué)會發(fā)現(xiàn),每一步最后都會睡眠一秒,才能打印結(jié)果,這是因?yàn)椴l(fā)執(zhí)行,goroutine還沒來得及返回結(jié)果,主線程已經(jīng)執(zhí)行完了。
那么如果不會面有沒有其他的方法?當(dāng)然有。第一種便是采用sync.WaitGroup來實(shí)現(xiàn)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine結(jié)束就登記-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 啟動一個(gè)goroutine就登記+1
go hello(i)
}
wg.Wait() // 等待所有登記的goroutine都結(jié)束
}
詳細(xì)用法詳見sync包。另外一種便是channel
channel
channel是用來傳遞數(shù)據(jù)的一個(gè)數(shù)據(jù)結(jié)構(gòu),同map一樣使用內(nèi)置的make來創(chuàng)建。如
ch := make(chan int) // 無緩沖通道 ch1 := make(chan int, 10) //緩沖為10的通道
channel類型
定義格式
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
它表示三種類型的定義,可選的<-表示channel的方向。如果沒有指定,即表示雙向通道,既可以接收,也可以發(fā)送。
chan T // 可以接收和發(fā)送類型為 T 的數(shù)據(jù) chan<- float64 // 只可以用來發(fā)送 float64 類型的數(shù)據(jù) <-chan int // 只可以用來接收 int 類型的數(shù)據(jù)
<-總是最優(yōu)先與最左邊類型結(jié)合。如
chan<- chan int // 等價(jià) chan<- (chan int) chan<- <-chan int // 等價(jià) chan<- (<-chan int) <-chan <-chan int // 等價(jià) <-chan (<-chan int) chan (<-chan int)
channel操作
常見三種操作,接收,發(fā)送和關(guān)閉
ch := make(chan int)
發(fā)送:ch <- 1 //將1發(fā)送到ch通道中
接收:x := <-ch // 從ch接收值并賦給x。也可以直接拋棄:<-ch
關(guān)閉:close(ch)
close時(shí)可以通過i, ok := <-c可以查看Channel的狀態(tài),判斷值是零值還是正常讀取的值。
c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false
無緩沖通道
需要注意的是,無緩沖通道上的發(fā)送操作將會阻塞,值到另一個(gè)goroutine在對立的通道上執(zhí)行接收操作。這時(shí)值才算完成。相反,如果接收操作先執(zhí)行,接收方goroutine將阻塞,直到另一個(gè)goroutine在同一個(gè)通道發(fā)送一個(gè)值。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 發(fā)送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 從通道 c 中接收
fmt.Println(x, y, x+y)
}
打印結(jié)果
-5 17 12
單向通道
當(dāng)程序演進(jìn)時(shí),將大的函數(shù)拆分為多個(gè)更小的是很自然的,在當(dāng)一個(gè)通道用作函數(shù)的行參時(shí),它幾乎總是被有意地限制不能發(fā)送或接收。為了將這種意圖可以比避免誤用,在go的類型系統(tǒng)提供了單向通道。僅僅導(dǎo)出發(fā)送或者接收操作。如類型chan <- int是一個(gè)只能發(fā)送的通道。反之 <- chan int是一個(gè)只能接收int類型通道。
func sum(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go sum(ch1)
go squarer(ch2, ch1)
}
有緩沖通道
緩沖通道有一個(gè)元素隊(duì)列,隊(duì)列的最大長度在創(chuàng)建時(shí)通過make的容量參數(shù)來設(shè)置。
ch := make(chan string, 3)
一個(gè)空的緩沖通道
緩沖通道上的發(fā)送操作在隊(duì)列的尾部插入一個(gè)元素,接收操作從隊(duì)列的頭部移除一個(gè)元素。如果通道滿了,發(fā)送操作將會阻塞所在的goroutine直到另一個(gè)goroutine對它進(jìn)行移除操作留出可用的空間。反過來,如果通道空了。執(zhí)行接收操作的goroutine阻塞,直到另一個(gè)goroutine在通道上發(fā)送數(shù)據(jù)。
func main() {
ch := make(chan string, 3)
ch <- "a"
ch <- "b"
ch <- "c"
fmt.Println("發(fā)送成功")
x := <-ch // 打印a
}
range
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
如上面的例子,range c 產(chǎn)生的迭代值為channel中發(fā)送的值,它會一直迭代直到channel關(guān)閉。如果此時(shí)close(c)關(guān)掉。程序會一直阻塞在for....range c 這一行。
select
select語句選擇一組可能的send操作和receive操作去處理。它類似switch,但是只是用來處理通訊(communication)操作。它的case可以是send語句,也可以是receive語句,亦或者default。receive語句可以將值賦值給一個(gè)或者兩個(gè)變量。它必須是一個(gè)receive操作。最多允許有一個(gè)default case,它可以放在case列表的任何位置,盡管我們大部分會將它放在最后。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
如果有同時(shí)多個(gè)case去處理,比如同時(shí)有多個(gè)channel可以接收數(shù)據(jù),那么Go會偽隨機(jī)的選擇一個(gè)case處理(pseudo-random)。如果沒有case需要處理,則會選擇default去處理,如果default case存在的情況下。如果沒有default case,則select語句會阻塞,直到某個(gè)case需要處理。
特別注意的是nil channel會一直被阻塞。如果沒有default: nil chanel會一直阻塞。
最后列出channel的幾種常用關(guān)系
小結(jié):
在處理并發(fā)時(shí)會發(fā)生數(shù)據(jù)錯(cuò)亂的情況,這時(shí)候就會用到鎖機(jī)制,如上面一開始介紹sync包。鎖將會在sync包中描述。
總結(jié)
以上是生活随笔為你收集整理的golang 之并发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何识别电脑硬件型号如何查看自己的电脑型
- 下一篇: 路由器怎么设定指定IP路由器如何指定网段