go并發(fā)控制利器-centext(鐵索連環(huán))
更便捷的goroutine控制利器- Context
首先要和大家說聲抱歉哈,由于工作上、生活上的某些瑣事,以至于造成本節(jié)的斷更。不過請不要悲傷。因為我在這期間也是做過詳細(xì)的復(fù)習(xí)的。我相信一定會讓你有更加深入的理解,同時也歡迎你向我提出不足。我們共同進步。話不多說,我相信你已經(jīng)迫不及待了。還在等什么?let‘s GO
在本文中,我首先會介紹context是什么,它有什么作用,以及如何使用,其中還會參雜一點個人的理解,以及部分源碼的了解。What are you waiting for?
Context:
來自官方文檔
Context包定義了上下文類型,該類型在API邊界之間以及進程之間傳遞截止日期,取消信號和其他請求范圍的值
對服務(wù)器的傳入請求應(yīng)創(chuàng)建一個Context,而對服務(wù)器的傳出調(diào)用應(yīng)接受一個Context。
它們之間的函數(shù)調(diào)用鏈必須傳播Context,可以選擇將其替換為使用WithCancel,WithDeadline,WithTimeout或WithValue創(chuàng)建的派生Context。取消上下文后,從該上下文派生的所有上下文也會被取消。
WithCancel,WithDeadline和WithTimeout函數(shù)采用Context(父級)并返回派生的Context(子級)和CancelFunc。調(diào)用CancelFunc會取消該子代及其子代,刪除父代對該子代的引用,并停止所有關(guān)聯(lián)的計時器。未能調(diào)用CancelFunc會使子代及其子代泄漏,直到父代被取消或計時器觸發(fā)。審核工具檢查所有控制流路徑上是否都使用了CancelFuncs。
使用上下文的程序應(yīng)遵循以下規(guī)則,以使各個包之間的接口保持一致,并使靜態(tài)分析工具可以檢查上下文傳播:
不要將上下文存儲在結(jié)構(gòu)類型中;而是將上下文明確傳遞給需要它的每個函數(shù)。 Context應(yīng)該是第一個參數(shù),通常命名為ctx:
func?DoSomething(ctx?context.Context,?arg?Arg)?error?{
//??...?use?ctx?...
}
即使函數(shù)允許,也不要傳遞nil Context。如果不確定使用哪個上下文,請傳遞context.TODO
僅將上下文值用于傳遞過程和API的請求范圍數(shù)據(jù),而不用于將可選參數(shù)傳遞給函數(shù)。
可以將相同的上下文傳遞給在不同goroutine中運行的函數(shù)。上下文可以安全地被多個goroutine同時使用
巴拉巴拉,說了一大堆,反正我一句沒懂,當(dāng)然我知道context是干嘛的,(尬~,不小心暴露了,學(xué)渣的本質(zhì)),說說我的理解以及使用建議
對服務(wù)器的傳入請求應(yīng)創(chuàng)建一個Context,而對服務(wù)器的傳出響應(yīng)也應(yīng)接受一個Context。
函數(shù)調(diào)用鏈必須傳播Context,也可以選擇將其替換為使用WithCancel,WithDeadline,WithTimeout或WithValue創(chuàng)建的派生Context(也就是子類context)。取消上下文后,從該上下文派生的所有上下文也會被取消
Context 不要放在結(jié)構(gòu)體中,要以參數(shù)的方式傳遞。
Context 作為函數(shù)的參數(shù)時,要放在第一位,也就是第一個參數(shù)。
要使用 context.Background 函數(shù)生成根節(jié)點的 Context,也就是最頂層的 Context。
Context 傳值要傳遞必須的值,而且要盡可能地少,不要什么都傳。
Context 多協(xié)程安全,可以在多個協(xié)程中放心使用。
go Context定義
Context 是Go 1.7 標(biāo)準(zhǔn)庫引入 的標(biāo)準(zhǔn)庫,中文譯作“上下文”,準(zhǔn)確說它是 goroutine 的上下文,包含 goroutine 的運行狀態(tài)、環(huán)境、現(xiàn)場等信息。
使用context,我們可以輕松優(yōu)雅的做到取消goroutine,超時時間,運行截止時間,k-v存儲等。它是并發(fā)安全的
隨著 context 包的引入,標(biāo)準(zhǔn)庫中很多接口因此加上了 context 參數(shù),例如 database/sql 包。context 幾乎成為了并發(fā)控制和超時控制的標(biāo)準(zhǔn)做法。
context.Context 類型的值可以協(xié)調(diào)多個 groutine 中的代碼執(zhí)行“取消”操作,并且可以存儲鍵值對。最重要的是它是并發(fā)安全的。
與它協(xié)作的 API 都可以由外部控制執(zhí)行“取消”操作,例如:取消一個 HTTP 請求的執(zhí)行。
止于這些么?當(dāng)然 不止,還有更多的騷操作,接下來讓我們一起拿下它吧。
引入
為什么需要使用context,理由一
一個協(xié)程啟動后,大部分情況需要等待里面的代碼執(zhí)行完畢,然后協(xié)程會自行退出。但需要讓協(xié)程提前退出怎么辦呢?
下面我們以一個小的示例,來逐漸了解context的妙用之一吧
package?main
import?(
"fmt"
"sync"
"time"
)
func?monitor(name?string)?{
//開啟for?select循環(huán),j進行后臺監(jiān)控
for?{
select?{
default:
fmt.Printf("Time:?%v?監(jiān)控者:%s,?正在監(jiān)控...\n",?time.Now().Unix(),?name)
}
//?sleep?1?second
time.Sleep(time.Second?*?5)
fmt.Printf("%s?監(jiān)控完成,一切正常,請指示?over...\n",?name)
}
}
func?main()?{
var?wg?sync.WaitGroup
//?Define?waiting?group
wg.Add(1)
go?func()?{
//?Execution?complete
defer?wg.Done()
monitor("天眼")
}()
//Exit?after?waiting
wg.Wait()
}
我們在這里實現(xiàn)了一個基本的groutine執(zhí)行的case
我們定義了等待組wait group,防止協(xié)程提前退出。關(guān)于wait group可參考上一篇文章,golang并發(fā)控制的心應(yīng)手。
他會周期性的運行,不斷打印監(jiān)控信息,例如
那么我們完成上述的那個需求提前退出,那么該怎么辦呢?其中一個方法就是定義一個全局的sign,其他地方可以通過修改這個sign發(fā)出停止監(jiān)控的指令。然后在協(xié)程中先檢查這個變量,如果發(fā)現(xiàn)被通知關(guān)閉就停止監(jiān)控,退出當(dāng)前協(xié)程。從而實現(xiàn)可控制提前退出。示例代碼如下
package?main
import?(
"fmt"
"sync"
"time"
)
func?monitor1(signCh?chan?bool,?MonitoringPeriod?time.Duration,?name?string,?)?{
//開啟for?select循環(huán),一直后臺監(jiān)控
for?{
select?{
case?<-signCh:
fmt.Println(name,?"停止指令已收到,停止...")
return
default:
fmt.Printf("Time:?%v?[監(jiān)控者]:%s,?正在監(jiān)控...\n",?time.Now().Unix(),?name)
}
time.Sleep(MonitoringPeriod?*?time.Second)
}
}
func?main()?{
var?wg?sync.WaitGroup
signCh?:=?make(chan?bool)?//sign?用來停止監(jiān)控
const?MonitoringTime,?MonitoringPeriod?=?20,?2
wg.Add(1)
go?func()?{
defer?wg.Done()
monitor1(signCh,?MonitoringPeriod,?"天眼")
}()
time.Sleep(MonitoringTime?*?time.Second)?//實施監(jiān)控時間
signCh?<-?true???????????????????????????//發(fā)停止指令
wg.Wait()
}
這樣我們就實現(xiàn)了,可控制話的groutine退出,但如果在新增幾個定期的任務(wù)功能,那該如何是好?
管他的,我們先把這個弄懂了先。老夫先干為敬。首先我們先看程序運行圖,如下
這個示例是使用 select+channel 的方式改造,實現(xiàn)了通過 channel 發(fā)送指令讓監(jiān)控狗停止,進而達到協(xié)程退出的目的。
首先我們定義了sync.WaitGroup,防止gorontine提前退出。signCh,他是一個bool值類型channel,用于發(fā)送sign后續(xù)的退出。
MonitoringTime,MonitoringPeriod,監(jiān)控時間與監(jiān)控周期。second。
然后創(chuàng)建goroutine執(zhí)行select+channel。
Go Context 初試體驗
為 函數(shù)增加 signCh 參數(shù),用于接收停止指令;
在 main 函數(shù)中,聲明用于停止的 signCh,傳遞給 monitor1 函數(shù),然后通過 signCh<-true 發(fā)送停止指令讓協(xié)程退出。
通過 select+channel 讓協(xié)程退出的方式比較優(yōu)雅,以下幾個問題也隨之凸顯
但如果我們希望做到同時取消很多個協(xié)程呢?
如果是定時取消協(xié)程又該怎么辦?
這時候 select+channel 的局限性就凸現(xiàn)出來了,即使定義了多個 channel 解決問題,當(dāng)然這個方式是可行的,但代碼邏輯也會非常復(fù)雜、難以維護。
要解決這種復(fù)雜的協(xié)程問題,必須有一種可以跟蹤協(xié)程的方案,只有跟蹤到每個協(xié)程,才能更好地控制它們,這種方案就是 Go 語言標(biāo)準(zhǔn)庫為我們提供的 Context,接下來我們體驗一下它的強大之處吧。
package?main
import?(
"context"
"fmt"
"sync"
"time"
)
func?main()?{
var?wg?sync.WaitGroup
const?MonitoringTime,?MonitoringPeriod?=?20,?2
wg.Add(1)
//?定義一個等待的?`context`
ctx,?stop?:=?context.WithCancel(context.Background())
go?func()?{
defer?wg.Done()
monitor2(ctx,?MonitoringPeriod,?"天眼")
}()
time.Sleep(MonitoringTime?*?time.Second)?//先監(jiān)控5秒
stop()???????????????????????????????????//發(fā)停止指令
wg.Wait()
}
func?monitor2(ctx?context.Context,?MonitoringPeriod?time.Duration,?name?string,?)?{
//開啟for?select循環(huán),一直后臺監(jiān)控
for?{
select?{
case?<-ctx.Done():
fmt.Println(name,?"停止指令已收到,停止...")
return
default:
fmt.Printf("Time:?%v?[監(jiān)控者]:%s,?正在監(jiān)控...\n",?time.Now().Unix(),?name)
}
time.Sleep(MonitoringPeriod?*?time.Second)
}
}
是不是很優(yōu)雅呢?確實如此,那么為什么也可以達到上面使用channel,的效果呢。那么我們?nèi)タ匆幌滤木唧w實現(xiàn)部分呢,
`WithCancel`
以下是WithCancel:具體實現(xiàn)部分代碼
WithCancel:返回具有新的“完成”通道的父級副本。當(dāng)調(diào)用返回的cancel函數(shù)或關(guān)閉父上下文的Done通道時(以先發(fā)生的為準(zhǔn)),將關(guān)閉返回的上下文的Done通道。取消此上下文將釋放與其關(guān)聯(lián)的資源,因此在此上下文中運行的操作完成后,代碼應(yīng)立即調(diào)用cancel。
func?WithCancel(parent?Context)?(ctx?Context,?cancel?CancelFunc)?{
if?parent?==?nil?{
panic("cannot?create?context?from?nil?parent")
}
c?:=?newCancelCtx(parent)
propagateCancel(parent,?&c)
return?&c,?func()?{?c.cancel(true,?Canceled)?}
}
除了WithCancel,之外還有WithDeadline,WithTimeout,WithValue,首先我們來繼續(xù)看看WithDeadline具體實現(xiàn),以及使用技巧吧
`WithTimeout`
func?WithDeadline(parent?Context,?d?time.Time)?(Context,?CancelFunc)?{
if?parent?==?nil?{
panic("cannot?create?context?from?nil?parent")
}
if?cur,?ok?:=?parent.Deadline();?ok?&&?cur.Before(d)?{
//?The?current?deadline?is?already?sooner?than?the?new?one.
return?WithCancel(parent)
}
c?:=?&timerCtx{
cancelCtx:?newCancelCtx(parent),
deadline:??d,
}
propagateCancel(parent,?c)
dur?:=?time.Until(d)
if?dur?<=?0?{
c.cancel(true,?DeadlineExceeded)?//?deadline?has?already?passed
return?c,?func()?{?c.cancel(false,?Canceled)?}
}
c.mu.Lock()
defer?c.mu.Unlock()
if?c.err?==?nil?{
c.timer?=?time.AfterFunc(dur,?func()?{
c.cancel(true,?DeadlineExceeded)
})
}
return?c,?func()?{?c.cancel(true,?Canceled)?}
}
func?WithTimeout(parent?Context,?timeout?time.Duration)?(Context,?CancelFunc)?{
return?WithDeadline(parent,?time.Now().Add(timeout))
}
取消此上下文將釋放與之關(guān)聯(lián)的資源,因此在此上下文中運行的操作完成后,代碼應(yīng)立即調(diào)用cancel:
來看一下具體如何使用吧,示例如下
package?main
import?(
"context"
"fmt"
"time"
)
func?main()?{
//?創(chuàng)建一個子節(jié)點的context,3秒后自動超時
const?MonitoringTime,?MonitoringPeriod?=?20,?2
ctx,?cancel?:=?context.WithTimeout(context.Background(),?time.Second*10)
go?func()?{
monitor4(ctx,?MonitoringPeriod,?"天眼")
fmt.Println("退出時間",time.Now().Unix())
}()
time.Sleep(MonitoringTime?*?time.Second)
cancel()
}
func?monitor4(ctx?context.Context,?MonitoringPeriod?time.Duration,?name?string,?)?{
//開啟for?select循環(huán),一直后臺監(jiān)控
for?{
select?{
case?<-ctx.Done():
fmt.Println(name,?"停止指令已收到,停止...")
return
default:
fmt.Printf("Time:?%v?[監(jiān)控者]:%s,?正在監(jiān)控...\n",?time.Now().Unix(),?name)
time.Sleep(MonitoringPeriod?*?time.Second)
}
}
}
以上會有兩種情況發(fā)生退出,
一、程序main退出,全局退出
二、我們定義的timeout退出
他們的基本性質(zhì)與使用我們就簡單的過了一遍,下面讓我們來個小結(jié)。
WithCancel(parent Context):生成一個可取消的 Context。
WithDeadline(parent Context, d time.Time):生成一個可定時取消的 Context,參數(shù) d 為定時取消的具體時間。
WithTimeout(parent Context, timeout time.Duration):生成一個可超時取消的 Context,參數(shù) timeout 用于設(shè)置多久后取消
WithValue(parent Context, key, val interface{}):生成一個可攜帶 key-value 鍵值對的 Context。
是不是發(fā)現(xiàn),其實也沒有那么難呢?當(dāng)然,它本來就很簡單,接下來我們來點更刺激的,同時取消多goroutine,啥也不說了,上~
package?main
import?(
"context"
"fmt"
"strconv"
"sync"
"time"
)
var?wg?sync.WaitGroup
func?main()?{
const?MonitoringTime,?MonitoringPeriod?=?20,?2
wg.Add(1)
ctx,?stop?:=?context.WithCancel(context.Background())
for?i?:=?0;?i?3;?i++?{
go?monitor6(ctx,?MonitoringPeriod,?strconv.Itoa(i))
}
time.Sleep(MonitoringTime?*?time.Second)?//先監(jiān)控5秒
stop()???????????????????????????????????//發(fā)停止指令
wg.Wait()
}
func?monitor6(ctx?context.Context,?MonitoringPeriod?time.Duration,?name?string,?)?{
defer?wg.Done()
//開啟for?select循環(huán),一直后臺監(jiān)控
for?{
select?{
case?<-ctx.Done():
fmt.Println(name,?"停止指令已收到,停止...")
return
default:
fmt.Printf("Time:?%v?[監(jiān)控者]:天眼%s,?正在監(jiān)控...\n",?time.Now().Unix(),?name)
}
time.Sleep(MonitoringPeriod?*?time.Second)
}
}
Time: 1612948086 [監(jiān)控者]:天眼0, 正在監(jiān)控…
Time: 1612948086 [監(jiān)控者]:天眼1, 正在監(jiān)控…
Time: 1612948086 [監(jiān)控者]:天眼2, 正在監(jiān)控…
… …
Time: 1612948104 [監(jiān)控者]:天眼2, 正在監(jiān)控…
Time: 1612948104 [監(jiān)控者]:天眼0, 正在監(jiān)控…
Time: 1612948104 [監(jiān)控者]:天眼1, 正在監(jiān)控…
2 停止指令已收到,停止…
1 停止指令已收到,停止…
0 停止指令已收到,停止…
你以為這樣就完了么,這只是一個小的case,它還可以管理子節(jié)點。其管理與樹形結(jié)構(gòu)十分的相似。
除此之外還可以傳遞值,接下來讓我們來看看吧
package?main
import?(
"context"
"fmt"
"sync"
"time"
)
func?main()?{
var?wg?sync.WaitGroup
ctx,?stop?:=?context.WithCancel(context.Background())
ctxVal?:=?context.WithValue(ctx,?"user",?"payne")
wg.Add(1)
go?func()?{
defer?wg.Done()
getValue(ctxVal)
}()
time.Sleep(3)
stop()
wg.Wait()
}
func?getValue(ctx?context.Context)?{
for?{
select?{
case?<-ctx.Done():
fmt.Println("exit")
return
default:
user?:=?ctx.Value("user")
fmt.Println("【獲取用戶】",?"用戶為:",?user)
time.Sleep(1?*?time.Second)
}
}
}
輸出如下
【獲取用戶】 用戶為: payne
exit…
總結(jié):
Context為我們主要定義四種方法WithDeadline,WithTimeout,WithValue,WithCancel,從而達到控制goroutine的目的,但卻不僅限于我們以上介紹的那樣(只介紹了一層,其實可以是多層。形成多對多的關(guān)系),它更深層次的使用你可以想象成多叉樹的情況。
context,這一篇就暫且完成啦,期待下一篇。并發(fā)模式
并發(fā)模式,故名思義。他與設(shè)計模式一樣,即使用goroutine并發(fā)的一些總結(jié)。
我將與你探討
Goroutine WorkPool:讓我們隨影所欲的控制創(chuàng)建gototine的數(shù)量,且復(fù)用。
Pipeline 模式,他像工廠流水線一般,我們將是這將其拆分歸并
扇出扇入模式,在pipline的基礎(chǔ)上對耗時較長的進行處理
Futures 模式,Pipeline 流水線模式中的工序是相互依賴的,但是在我們的實際需求中,也有大量的任務(wù)之間相互獨立、沒有依賴,所以為了提高性能,這些獨立的任務(wù)就可以并發(fā)執(zhí)行。
期待~
API Go
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。