摘要:比如主協(xié)程啟動(dòng)個(gè)子協(xié)程,主協(xié)程等待所有子協(xié)程退出后再繼續(xù)后續(xù)流程,這種場(chǎng)景下也可輕易實(shí)現(xiàn)。這個(gè)例子中,父協(xié)程僅僅是等待子協(xié)程結(jié)束,其實(shí)父協(xié)程也可以向管道中寫(xiě)入數(shù)據(jù)通知子協(xié)程結(jié)束,這時(shí)子協(xié)程需要定期地探測(cè)管道中是否有消息出現(xiàn)。
Go 語(yǔ)言中最常見(jiàn)的、也是經(jīng)常被人提及的設(shè)計(jì)模式就是:
"不要通過(guò)共享內(nèi)存來(lái)通信,我們應(yīng)該使用通信來(lái)共享內(nèi)存"
通過(guò)共享內(nèi)存來(lái)通信是直接讀取內(nèi)存的數(shù)據(jù),而通過(guò)通信來(lái)共享內(nèi)存,是通過(guò)發(fā)送消息的方式來(lái)進(jìn)行同步。
而通過(guò)發(fā)送消息來(lái)同步的這種方式常見(jiàn)的就是 Go 采用的通信順序進(jìn)程 CSP(Communication Sequential Process) 模型以及 Erlang 采用的 Actor 模型,這兩種方式都是通過(guò)通信來(lái)共享內(nèi)存。
如下圖所示
大部分的語(yǔ)言采用的都是第一種方式直接去操作內(nèi)存,然后通過(guò)互斥鎖,CAS 等操作來(lái)保證并發(fā)安全。Go 引入了 Channel 和 Goroutine 實(shí)現(xiàn) CSP 模型來(lái)解耦這個(gè)操作。
優(yōu)點(diǎn):
缺點(diǎn):
目前的 Channel 收發(fā)操作均遵循了先進(jìn)先出的設(shè)計(jì),具體規(guī)則如下:
鎖(Lock) 是一種常見(jiàn)的并發(fā)控制技術(shù),我們一般會(huì)將鎖分成樂(lè)觀鎖 和 悲觀鎖,即樂(lè)觀并發(fā)控制和悲觀并發(fā)控制,無(wú)鎖(lock-free)隊(duì)列更準(zhǔn)確的描述是使用樂(lè)觀并發(fā)控制的隊(duì)列。樂(lè)觀并發(fā)控制也叫樂(lè)觀鎖,很多人都會(huì)誤以為樂(lè)觀鎖是與悲觀鎖差不多,然而它并不是真正的鎖,只是一種并發(fā)控制的思想.
樂(lè)觀并發(fā)控制本質(zhì)上是基于驗(yàn)證的協(xié)議,我們使用原子指令 CAS(compare-and-swap 或者 compare-and-set)在多線程中同步數(shù)據(jù),無(wú)鎖隊(duì)列的實(shí)現(xiàn)也依賴(lài)這一原子指令。
從某種程度上說(shuō),Channel 是一個(gè)用于同步和通信的有鎖隊(duì)列,使用互斥鎖解決程序中可能存在的線程競(jìng)爭(zhēng)問(wèn)題
Go 語(yǔ)言社區(qū)也在 2014 年提出了無(wú)鎖 Channel 的實(shí)現(xiàn)方案,該方案將 Channel 分成了以下三種類(lèi)型:
同步 Channel — 無(wú)緩沖區(qū),發(fā)送方會(huì)直接將數(shù)據(jù)交給(Handoff)接收方
異步channel: 基于環(huán)形緩存的傳統(tǒng)生產(chǎn)者消費(fèi)者模型;
chan struct{} 類(lèi)型的異步 Channel — struct{} 類(lèi)型不占用內(nèi)存空間,不需要實(shí)現(xiàn)緩沖區(qū)和直接發(fā)送(Handoff)的語(yǔ)義;
Go 語(yǔ)言的 Channel 在運(yùn)行時(shí)使用 runtime.hchan 結(jié)構(gòu)體表示。我們?cè)?Go 語(yǔ)言中創(chuàng)建新的 Channel 時(shí),實(shí)際上創(chuàng)建的都是如下所示的結(jié)構(gòu):
type hchan struct { qcount uint // 隊(duì)列中元素總數(shù)量 dataqsiz uint // 循環(huán)隊(duì)列的長(zhǎng)度 buf unsafe.Pointer // 指向長(zhǎng)度為 dataqsiz 的底層數(shù)組,只有在有緩沖時(shí)這個(gè)才有意義 elemsize uint16 // 能夠發(fā)送和接受的元素大小 closed uint32 // 是否關(guān)閉 elemtype *_type // 元素的類(lèi)型 sendx uint // 當(dāng)前已發(fā)送的元素在隊(duì)列當(dāng)中的索引位置 recvx uint // 當(dāng)前已接收的元素在隊(duì)列當(dāng)中的索引位置 recvq waitq // 接收 Goroutine 鏈表 sendq waitq // 發(fā)送 Goroutine 鏈表 lock mutex // 互斥鎖}// waitq 是一個(gè)雙向鏈表,里面保存了 goroutinetype waitq struct { first *sudog last *sudog}
如下圖所示,channel 底層其實(shí)是一個(gè)循環(huán)隊(duì)列
Go 語(yǔ)言中所有 Channel 的創(chuàng)建都會(huì)使用 make 關(guān)鍵字。創(chuàng)建的表達(dá)式使用 make(chan T, cap)
來(lái)創(chuàng)建 channel.
如果不向 make 傳遞表示緩沖區(qū)大小的參數(shù),那么就會(huì)設(shè)置一個(gè)默認(rèn)值 0,也就是當(dāng)前的 Channel 不存在緩沖區(qū)。
當(dāng)想要向 Channel
發(fā)送數(shù)據(jù)時(shí),就需要使用 ch <- i
語(yǔ)句.
在發(fā)送數(shù)據(jù)的邏輯執(zhí)行之前會(huì)先為當(dāng)前 Channel 加鎖,防止多個(gè)線程并發(fā)修改數(shù)據(jù)。
如果 Channel 已經(jīng)關(guān)閉,那么向該 Channel 發(fā)送數(shù)據(jù)時(shí)會(huì)報(bào) “send on closed channel” 錯(cuò)誤并中止程序。
如果 Channel 沒(méi)有被關(guān)閉并且已經(jīng)有處于讀等待的 Goroutine,會(huì)取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù):
直接發(fā)送的過(guò)程稱(chēng)為兩個(gè)部分:
runtime.sendDirect
將發(fā)送的數(shù)據(jù)直接拷貝到 x = <-c 表達(dá)式中變量 x 所在的內(nèi)存地址上;runtime.goready
將等待接收數(shù)據(jù)的 Goroutine 標(biāo)記成可運(yùn)行狀態(tài) Grunnable 并把該 Goroutine 放到發(fā)送方所在的處理器的 runnext 上等待執(zhí)行,該處理器在下一次調(diào)度時(shí)會(huì)立刻喚醒數(shù)據(jù)的接收方;需要注意的是,發(fā)送數(shù)據(jù)的過(guò)程只是將接收方的 Goroutine 放到了處理器的 runnext 中,程序沒(méi)有立刻執(zhí)行該 Goroutine。
如果創(chuàng)建的 Channel 包含緩沖區(qū)并且 Channel 中的數(shù)據(jù)沒(méi)有裝滿,會(huì)使用 runtime.chanbuf
計(jì)算出下一個(gè)可以存儲(chǔ)數(shù)據(jù)的位置,然后通過(guò) runtime.typedmemmove
將發(fā)送的數(shù)據(jù)拷貝到緩沖區(qū)中并增加 sendx 索引和 qcount 計(jì)數(shù)器。
當(dāng) Channel 沒(méi)有接收者能夠處理數(shù)據(jù)時(shí),向 Channel 發(fā)送數(shù)據(jù)會(huì)被下游阻塞,當(dāng)然使用 select 關(guān)鍵字可以向 Channel 非阻塞地發(fā)送消息。
可以簡(jiǎn)單梳理和總結(jié)一下使用 ch <- i
表達(dá)式向 Channel 發(fā)送數(shù)據(jù)時(shí)遇到的幾種情況:
可以使用兩種不同的方式去接收 Channel 中的數(shù)據(jù):
i <- chi, ok <- ch
會(huì)根據(jù)緩沖區(qū)的大小分別處理不同的情況
當(dāng) Channel 的緩沖區(qū)中已經(jīng)包含數(shù)據(jù)時(shí),從 Channel 中接收數(shù)據(jù)會(huì)直接從緩沖區(qū)中 的索引位置中取出數(shù)據(jù)進(jìn)行處理:
當(dāng) Channel 的發(fā)送隊(duì)列中不存在等待的 Goroutine 并且緩沖區(qū)中也不存在任何數(shù)據(jù)時(shí),從管道中接收數(shù)據(jù)的操作會(huì)變成阻塞的,然而不是所有的接收操作都是阻塞的,與 select 語(yǔ)句結(jié)合使用時(shí)就可能會(huì)使用到非阻塞的接收操作:
使用 close(ch) 來(lái)關(guān)閉 channel 最后會(huì)調(diào)用 runtime 中的 closechan 方法.
channel一般用于協(xié)程之間的通信,channel也可以用于并發(fā)控制。比如主協(xié)程啟動(dòng)N個(gè)子協(xié)程,主協(xié)程等待所有子協(xié)程退出后再繼續(xù)后續(xù)流程,這種場(chǎng)景下channel也可輕易實(shí)現(xiàn)。
package mainimport ( "time" "fmt")func Process(ch chan int) { //Do some work... time.Sleep(time.Second) ch <- 1 //管道中寫(xiě)入一個(gè)元素表示當(dāng)前協(xié)程已結(jié)束}func main() { channels := make([]chan int, 10) //創(chuàng)建一個(gè)10個(gè)元素的切片,元素類(lèi)型為channel for i:= 0; i < 10; i++ { channels[i] = make(chan int) //切片中放入一個(gè)channel go Process(channels[i]) //啟動(dòng)協(xié)程,傳一個(gè)管道用于通信 } for i, ch := range channels { //遍歷切片,等待子協(xié)程結(jié)束 <-ch fmt.Println("Routine ", i, " quit!") }}
輸出:
Routine 0 quit!Routine 1 quit!Routine 2 quit!Routine 3 quit!Routine 4 quit!Routine 5 quit!Routine 6 quit!Routine 7 quit!Routine 8 quit!Routine 9 quit!
上面程序通過(guò)創(chuàng)建N個(gè)channel來(lái)管理N個(gè)協(xié)程,每個(gè)協(xié)程都有一個(gè)channel用于跟父協(xié)程通信,父協(xié)程創(chuàng)建完所有協(xié)程后等待所有協(xié)程結(jié)束。
這個(gè)例子中,父協(xié)程僅僅是等待子協(xié)程結(jié)束,其實(shí)父協(xié)程也可以向管道中寫(xiě)入數(shù)據(jù)通知子協(xié)程結(jié)束,這時(shí)子協(xié)程需要定期地探測(cè)管道中是否有消息出現(xiàn)。
關(guān)閉 channel 時(shí)會(huì)釋放所有阻塞的 Goroutine,所以我們就可以利用這個(gè)特性來(lái)做一對(duì)多的通知,除了一對(duì)多之外我們還用了 done 做了多對(duì)一的通知,當(dāng)然多對(duì)一這種情況還是建議直接使用 WaitGroup 即可
package mainimport ( "fmt" "time")func run(stop <-chan struct{}, done chan<- struct{}) { // 每一秒打印一次 for { select { case <-stop: fmt.Println("stop...") // 接收到停止后,向 done 管道中發(fā)送數(shù)據(jù),然后退出函數(shù) done <- struct{}{} return // 超時(shí)1秒將輸出hello case <-time.After(time.Second): fmt.Println("hello...") } }}func main() { // 一對(duì)多,使用無(wú)緩沖通道,當(dāng)關(guān)閉chan后,其他程序中接收到關(guān)閉信號(hào)后會(huì)統(tǒng)一執(zhí)行操作 stop := make(chan struct{}) // 多對(duì)一,當(dāng)關(guān)閉后,關(guān)閉一個(gè)chan, 寫(xiě)入一個(gè)數(shù)據(jù)到管道中 done := make(chan struct{}, 10) for i := 0; i < 10; i++ { go run(stop, done) } // 模擬超時(shí)時(shí)間 time.Sleep(5 * time.Second) close(stop) for i := 0; i < 10; i++ { <-done }}
輸出:
hello...hello...hello......hello..stop...stop...stop...stop...stop...stop...stop...stop...stop...stop...
利用無(wú)緩沖channel,接收早于發(fā)送的特點(diǎn),只有當(dāng)數(shù)據(jù)寫(xiě)入后,接收才能完成實(shí)現(xiàn)數(shù)據(jù)一致性
package mainimport ( "fmt")// 這里只能讀func read(c <-chan int) { fmt.Println("read:", <-c)}// 這里只能寫(xiě)func write(c chan<- int) { c <- 0}func main() { c := make(chan int) go write(c) read(c)}
超時(shí)控制還是建議使用 context
func run(stop <-chan struct{}, done chan<- struct{}) { // 每一秒打印一次 hello for { select { case <-stop: fmt.Println("stop...") done <- struct{}{} return case <-time.After(time.Second): fmt.Println("hello") } }}
根據(jù)控制Channel的緩存大小來(lái)控制并發(fā)執(zhí)行的Goroutine的最大數(shù)目
var limit = make(chan int, 3)func main() { for _, w := range work { go func() { limit <- 1 w() <-limit }() } select{}}
最后一句select{}是一個(gè)空的管道選擇語(yǔ)句,該語(yǔ)句會(huì)導(dǎo)致main線程阻塞,從而避免程序過(guò)早退出。還有for{}
、<-make(chan int)
等諸多方法可以達(dá)到類(lèi)似的效果。因?yàn)閙ain線程被阻塞了,如果需要程序正常退出的話可以通過(guò)調(diào)用os.Exit(0)實(shí)現(xiàn)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/124536.html
摘要:的用例的用法最早是語(yǔ)言傳開(kāi)來(lái)的看一下我從網(wǎng)上扒的代碼其中符號(hào)是往當(dāng)中寫(xiě)入數(shù)據(jù)的操作同時(shí)注意一般的位置對(duì)于來(lái)說(shuō)是阻塞的由于能夠處理異步操作也就是說(shuō)能做到異步代碼用同步寫(xiě)法更多的細(xì)節(jié)搜索應(yīng)該就能找到除了也實(shí)現(xiàn)了對(duì)于的支持也就是 CSP 的用例 CSP 的用法最早是 Go 語(yǔ)言傳開(kāi)來(lái)的, 看一下我從網(wǎng)上扒的代碼: package main import fmt func ping(pin...
摘要:協(xié)程與信箱得益于,我們可以基于的協(xié)程與快速實(shí)現(xiàn)一個(gè)信箱模式調(diào)度。樣例代碼比如在一個(gè)聊天室中,我們可以定義一個(gè)房間模型。 什么是Actor? Actor對(duì)于PHPer來(lái)說(shuō),可能會(huì)比較陌生,寫(xiě)過(guò)Java的同學(xué)會(huì)比較熟悉,Java一直都有線程的概念(雖然PHP有Pthread,但不普及),它是一種非共享內(nèi)存的并發(fā)模型,每個(gè)Actor內(nèi)的數(shù)據(jù)獨(dú)立存在,Actor之間通過(guò)消息傳遞的形式進(jìn)行交互調(diào)...
摘要:為語(yǔ)言提供了強(qiáng)大的協(xié)程編程模式。提供的協(xié)程語(yǔ)法借鑒自,在此向開(kāi)發(fā)組致敬協(xié)程可以與很好地互補(bǔ)。并發(fā)執(zhí)行使用創(chuàng)建協(xié)程,可以讓和兩個(gè)函數(shù)變成并發(fā)執(zhí)行。協(xié)程需要拿到請(qǐng)求的結(jié)果。 Swoole4為PHP語(yǔ)言提供了強(qiáng)大的CSP協(xié)程編程模式。底層提供了3個(gè)關(guān)鍵詞,可以方便地實(shí)現(xiàn)各類(lèi)功能。 Swoole4提供的PHP協(xié)程語(yǔ)法借鑒自Golang,在此向GO開(kāi)發(fā)組致敬 PHP+Swoole協(xié)程可以與...
摘要:它避免了上下文切換的額外耗費(fèi),兼顧了多線程的優(yōu)點(diǎn),簡(jiǎn)化了高并發(fā)程序的復(fù)雜。而可以理解為一種語(yǔ)言的協(xié)程。線程輕量級(jí)進(jìn)程,,是程序執(zhí)行流的最小單元。一個(gè)標(biāo)準(zhǔn)的線程由線程,當(dāng)前指令指針,寄存器集合和堆棧組成。其實(shí)就是或者等語(yǔ)言中的多線程開(kāi)發(fā)。 grape 全部視頻:https://segmentfault.com/a/11... 原視頻地址:https://biglive.xueersi.c...
閱讀 1290·2021-11-23 09:51
閱讀 709·2021-11-19 09:40
閱讀 1372·2021-10-11 10:58
閱讀 2395·2021-09-30 09:47
閱讀 3765·2021-09-22 15:55
閱讀 2199·2021-09-03 10:49
閱讀 1291·2021-09-03 10:33
閱讀 723·2019-08-29 17:12