go并發編程
- 一、 并發介紹
- 1,進程和線程
- 2,并發和并行
- 3,協程和線程
- 4,goroutine
- 二、 Goroutine
- 1,使用goroutine
- 1)啟動單個goroutine
- 2)啟動多個goroutine
- 2,goroutine與線程
- 3,goroutine調度
- 三、runtime包
- 1,runtime.Gosched()
- 2,runtime.Goexit() 退出當時協程
- 3, runtime.GOMAXPROCS
- 4,將任務分配到不同的CPU邏輯核心上實現并行
- 5,Go語言中的操作系統線程和goroutine的關系:
- 四、channel
- 1,CSP模型
- 2,channel 類型
- 3,創建channel
- 4,channel 操作
- 1)發送
- 2)接收
- 3)關閉
- 5,無緩沖通道
- 6,有緩沖通道
- 7,從通道中遍歷獲取值
- 8,單向通道
- 8,通道總結
一、 并發介紹
1,進程和線程
- A. 進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。
- B. 線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
- C.一個進程可以創建和撤銷多個線程;同一個進程中的多個線程之間可以并發執行
2,并發和并行
- A. 多線程程序在一個核的cpu上運行,就是并發。
- B. 多線程程序在多個核的cpu上運行,就是并行。
并發:
并行:
3,協程和線程
- 協程:獨立的棧空間,共享堆空間,調度由用戶自己控制,本質上有點類似于用戶級線程,這些用戶級線程的調度也是自己實現的。
- 線程:一個線程上可以跑多個協程,協程是輕量級的線程。
4,goroutine
- goroutine 只是由官方實現的超級"線程池"。
- 每個實力4~5KB的棧內存占用和由于實現機制而大幅減少的創建和銷毀開銷是go高并發的根本原因
- goroutine 奉行通過通信來共享內存,而不是共享內存來通信。
二、 Goroutine
Go語言引入了goroutine機制,簡化了并發編程。程序員只需定義任務函數,通過開啟goroutine實現并發執行,而無需自己管理線程池、任務調度和上下文切換。
Go運行時負責智能分配任務到CPU,將復雜性隱藏在底層。
這使得Go成為現代化編程語言,使并發編程更加簡單和高效。
1,使用goroutine
- 在函數,或匿名函數 前面添加 go 關鍵詞。
- 一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。
1)啟動單個goroutine
func hello() {fmt.Println("Hello Goroutine!")
}
func main() {go hello()fmt.Println("main goroutine done!")
}//但打印沒有 ,Hello Goroutine! ,因為main 生命周期結束,goroutine 還沒啟動//讓main 等等 goroutine 粗暴方法:func main() {go hello() // 啟動另外一個goroutine去執行hello函數fmt.Println("main goroutine done!")time.Sleep(time.Second)
}
2)啟動多個goroutine
- 使用了sync.WaitGroup來實現goroutine的同步
var wg sync.WaitGroupfunc hello(i int) {defer wg.Done() // goroutine結束就登記-1fmt.Println("Hello Goroutine!", i)
}
func main() {for i := 0; i < 10; i++ {wg.Add(1) // 啟動一個goroutine就登記+1go hello(i)}wg.Wait() // 等待所有登記的goroutine都結束
}打印出來,并不是順序,因為這是因為10個goroutine是并發執行的,而goroutine的調度是隨機的。
2,goroutine與線程
- 可增長棧
OS線程(操作系統線程)一般都有固定的棧內存(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
3,goroutine調度
Go語言的運行時(runtime)引入了GPM模型來實現并發調度,與傳統操作系統調度不同。
-
G(goroutine): G是goroutine的縮寫,代表一個任務單元。它存儲了該任務的信息,以及與所在P(處理器)的關聯。
-
P(處理器): P管理一組goroutine隊列,包含當前goroutine的運行上下文。P負責調度自己的隊列,比如暫停耗時長的任務、切換到其他任務。當P隊列為空,它會從全局隊列取任務,甚至從其他P隊列搶占任務。
-
M(機器): M是Go運行時對操作系統內核線程的虛擬。通常是一一對應的關系,每個M執行一個goroutine。當一個G長時間阻塞在一個M上,會創建新的M,將其他G掛載在新M上。舊M釋放后,用于回收資源。
-
GOMAXPROCS: 用于設定P的個數,控制并發度,但不會過多地增加P和M,以避免頻繁切換的開銷。
Go語言與其他語言不同之處在于,它在運行時實現了自己的調度器,使用m:n調度技術。這意味著goroutine的調度發生在用戶態,避免了內核態與用戶態的頻繁切換,包括內存分配與釋放都在用戶態維護,性能開銷較小。此外,Go語言充分利用多核硬件資源,將多個goroutine均勻分配在物理線程上,加上goroutine的輕量特性,保證了高效的并發調度性能。
三、runtime包
1,runtime.Gosched()
一種協作式多任務切換的方式,讓正在運行的 goroutine 暫時停下來,讓其他等待執行的 goroutine 有機會運行。
package mainimport ("fmt""runtime"
)func main() {go func(s string) {for i := 0; i < 5; i++ {fmt.Println(s)}}("world")// 主程for i := 0; i < 2; i++ {//切換 再次分配任務runtime.Gosched()fmt.Println("hello")}
}
2,runtime.Goexit() 退出當時協程
package mainimport ("fmt""runtime"
)func main() {go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")// 結束協程runtime.Goexit()defer fmt.Println("C.defer")fmt.Println("B")}()fmt.Println("A")}()for {}
}
3, runtime.GOMAXPROCS
-
Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼。默認值是機器上的CPU核心數。例如在一個8核心的機器上,調度器會把Go代碼同時調度到8個OS線程上(GOMAXPROCS是m:n調度中的n)。
-
Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序并發時占用的CPU邏輯核心數。
-
Go1.5版本之前,默認使用的是單核心執行。Go1.5版本之后,默認使用全部的CPU邏輯核心數。
4,將任務分配到不同的CPU邏輯核心上實現并行
- 單核心
package mainimport ("fmt""runtime""time"
)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)
}輸出,看出是執行完一個goroutine ,再執行另一個
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
- 多核心
package mainimport ("fmt""runtime""time"
)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)
}輸出,看出是并發執行
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 9
5,Go語言中的操作系統線程和goroutine的關系:
- 1.一個操作系統線程對應用戶態多個goroutine。
- 2.go程序可以同時使用多個操作系統線程。
- 3.goroutine和OS線程是多對多的關系,即m:n
四、channel
1,CSP模型
并發執行函數的目的是讓多個任務同時進行,但僅僅并發執行函數是不夠的,因為這些函數可能需要相互交換數據。在并發環境中,共享內存雖然可以用于數據交換,但容易引發競態問題,而使用互斥量會影響性能。
Go語言采用了CSP(Communicating Sequential Processes)并發模型,強調通過通信來共享數據,而不是通過共享數據來進行通信。這種方式更加安全且高效。
-
關鍵點:
-
CSP模型: Go語言采用了CSP模型,強調通過通信來實現協程(goroutine)間的數據交換,而不是直接共享內存。
-
通道(channel): 通道是用于協程間通信的一種機制,類似于一個隊列,保證了數據的順序性。通過在通道中發送和接收數據,協程可以安全地進行交互。
-
通道的特點: 每個通道都有特定的元素類型,通道的操作遵循先進先出原則。通過通道的發送和接收操作,協程之間可以安全地進行數據交換,避免了競態問題。
-
并發優勢: 通過通道,Go語言實現了安全且高效的并發編程,允許協程在不同任務之間進行數據交換,而不需要顯式地使用互斥量進行加鎖。
-
通過使用通道,Go語言的并發模型強調了協程之間通過通信共享數據,而不是通過共享數據來進行通信,從而避免了許多傳統并發模型中常見的問題。這使得并發編程更加安全、簡潔和高效。
2,channel 類型
- channel 是一種類型,引用類型
聲明格式:var 變量 chan 元素類型
例如:var ch1 chan int // 聲明一個傳遞整型的通道var ch2 chan bool // 聲明一個傳遞布爾型的通道var ch3 chan []int // 聲明一個傳遞int切片的通道
3,創建channel
通道是引用類型,通道類型的空值是nil。var ch chan int
fmt.Println(ch) // <nil>聲明的通道后需要使用make函數初始化之后才能使用。創建channel的格式如下:make(chan 元素類型, [緩沖大小]) // 緩沖大小可選例如:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
4,channel 操作
1)發送
ch <- 10 // 把10發送到ch中
2)接收
x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值,忽略結果
3)關閉
close(ch)
- 只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。
- 通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之后關閉文件是必須要做的,但關閉通道不是必須的。
注意:
1.對一個關閉的通道再發送值就會導致panic。2.對一個關閉的通道進行接收會一直獲取值直到通道為空。3.對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值。4.關閉一個已經關閉的通道會導致panic。
5,無緩沖通道
func main() {ch := make(chan int)ch <- 10fmt.Println("發送成功")
}//出現以下錯誤fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:main.main().../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
- 無緩沖的通道必須有接收才能發送。
- 上面的代碼會阻塞在ch <- 10這一行代碼形成死鎖
啟動一個goroutine 解決該問題:
func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
func main() {ch := make(chan int)go recv(ch) // 啟用goroutine從通道接收值ch <- 10fmt.Println("發送成功")
}
無緩沖通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時值才能發送成功,兩個goroutine將繼續執行。相反,如果接收操作先執行,接收方的goroutine將阻塞,直到另一個goroutine在該通道上發送一個值。
使用無緩沖通道進行通信將導致發送和接收的goroutine同步化。因此,無緩沖通道也被稱為同步通道。
6,有緩沖通道
func main() {ch := make(chan int, 1) // 創建一個容量為1的有緩沖區通道ch <- 10fmt.Println("發送成功")
}
- 只要通道的容量大于零,那么該通道就是有緩沖的通道,通道的容量表示通道中能存放元素的數量。
- 可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量。
7,從通道中遍歷獲取值
package mainimport "fmt"func main() {ch1 := make(chan int)ch2 := make(chan int)// 開啟goroutine 將 0~100 的數發到 ch1 中go func() {for i := 0; i < 100; i++ {ch1 <- i}close(ch1)}()// 開啟goroutine 從ch1中接收值,發送給ch2go func() {for {i, ok := <-ch1if !ok {break}ch2 <- i * i}close(ch2)}()// 在主goroutine 打印ch2for i := range ch2 {fmt.Println("ch2:", i)}
}
- 能從關閉通道中獲取值
- 通過遍歷獲取通道中(關閉的通道也行)的值
8,單向通道
func counter(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 counter(ch1)go squarer(ch2, ch1)printer(ch2)
}
- 1.chan<- int是一個只能發送的通道,可以發送但是不能接收;
- 2.<-chan int是一個只能接收的通道,可以接收但是不能發送。