Golang中常用的并發模型有三種:
* 通過channel通知實現并發控制
無緩沖的通道指的是通道的大小為0,也就是說,這種類型的通道在接收前沒有能力保存任何值,它要求發送 goroutine 和接收 goroutine 同時準備好,才可以完成發送和接收操作。
從上面無緩沖的通道定義來看,發送 goroutine 和接收 gouroutine 必須是同步的,同時準備后,如果沒有同時準備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作準備好為止。這種無緩沖的通道我們也稱之為同步通道。
~~~go
func main() {
ch := make(chan struct{})
go func() {
fmt.Println("start working")
time.Sleep(time.Second * 1)
ch <- struct{}{}
}()
<-ch
fmt.Println("finished")
}
~~~
當主 goroutine 運行到`<-ch`接受 channel 的值的時候,如果該 channel 中沒有數據,就會一直阻塞等待,直到有值。 這樣就可以簡單實現并發控制
* 通過sync包中的WaitGroup實現并發控制
Goroutine是異步執行的,有的時候為了防止在結束main函數的時候結束掉Goroutine,所以需要同步等待,這個時候就需要用 WaitGroup了,在Sync包中,提供了 WaitGroup,它會等待它收集的所有 goroutine 任務全部完成。
在WaitGroup里主要有三個方法:
* Add, 可以添加或減少 goroutine的數量.
* Done, 相當于Add(-1).
* Wait, 執行后會堵塞主線程,直到WaitGroup 里的值減至0.
在主goroutine 中 Add(delta int) 索要等待goroutine 的數量。在每一個goroutine 完成后Done()表示這一個goroutine 已經完成,當所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回。
~~~go
func main(){
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
}
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
http.Get(url)
}(url)
}
wg.Wait()
}
~~~
在Golang官網中對于WaitGroup介紹是`A WaitGroup must not be copied after first use`,在 WaitGroup 第一次使用后,不能被拷貝。
應用示例:
~~~go
func main(){
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg sync.WaitGroup, i int) {
fmt.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
fmt.Println("exit")
}
~~~
運行:
~~~go
i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000094018)
/home/keke/soft/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc000094010)
/home/keke/soft/go/src/sync/waitgroup.go:130 +0x64
main.main()
/home/keke/go/Test/wait.go:17 +0xab
exit status 2
~~~
它提示所有的`goroutine`都已經睡眠了,出現了死鎖。這是因為 wg 給拷貝傳遞到了 goroutine 中,導致只有 Add 操作,其實 Done操作是在 wg 的副本執行的。
因此 Wait 就會死鎖。
這個第一個修改方式: 將匿名函數中 wg 的傳入類型改為`*sync.WaitGroup`,這樣就能引用到正確的`WaitGroup`了。
這個第二個修改方式: 將匿名函數中的 wg 的傳入參數去掉,因為Go支持閉包類型,在匿名函數中可以直接使用外面的 wg 變量.
* 在Go 1.7以后引進的強大的Context上下文,實現并發控制.
通常,在一些簡單場景下使用 channel 和 WaitGroup 已經足夠了,但是當面臨一些復雜多變的網絡并發場景下`channel`和`WaitGroup`顯得有些力不從心了。
比如一個網絡請求 Request,每個 Request 都需要開啟一個 goroutine 做一些事情,這些 goroutine 又可能會開啟其他的 goroutine,比如數據庫和RPC服務。
所以我們需要一種可以跟蹤 goroutine 的方案,才可以達到控制他們的目的,這就是Go語言為我們提供的 Context,稱之為上下文非常貼切,它就是goroutine 的上下文。
它是包括一個程序的運行環境、現場和快照等。每個程序要運行時,都需要知道當前程序的運行狀態,通常Go 將這些封裝在一個 Context 里,再將它傳給要執行的 goroutine 。
context 包主要是用來處理多個 goroutine 之間共享數據,及多個 goroutine 的管理。
context 包的核心是 struct Context,接口聲明如下:
~~~go
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
// Done() 返回一個只能接受數據的channel類型,當該context關閉或者超時時間到了的時候,該channel就會有一個取消信號
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
// Err() 在Done() 之后,返回context 取消的原因。
Err() error
// Deadline returns the time when this Context will be canceled, if any.
// Deadline() 設置該context cancel的時間點
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
// Value() 方法允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。
Value(key interface{}) interface{}
}
~~~
Context 對象是線程安全的,你可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行取消操作時,所有 goroutine 都會接收到取消信號。
一個 Context 不能擁有 Cancel 方法,同時我們也只能 Done channel 接收數據。其中的原因是一致的:接收取消信號的函數和發送信號的函數通常不是一個。
典型的場景是:父操作為子操作操作啟動 goroutine,子操作也就不能取消父操作。
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些