今天小編給大家分享一下go語言同步機制是什么及怎么實現(xiàn)的相關(guān)知識點,內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:域名注冊、網(wǎng)站空間、營銷軟件、網(wǎng)站建設(shè)、婺城網(wǎng)站維護(hù)、網(wǎng)站推廣。
go同步機制有:1、channel,著重并發(fā)問題中的數(shù)據(jù)流動,把流動的數(shù)據(jù)放到channel中,就能使用channel解決這個并發(fā);2、Sync.Mutex,擁有Lock、Unlock兩個方法,主要實現(xiàn)思想體現(xiàn)在Lock函數(shù)中;3、Sync.waitGroup;4、Sync.Once;5、Sync.context;6、Sync.pool;7、atomic包,針對變量進(jìn)行操作。
Golang的提供的同步機制有sync模塊下的Mutex、WaitGroup以及語言自身提供的chan等。
概述
Golang以如此明顯的方式告訴我們:。
優(yōu)點:channel的核心是數(shù)據(jù)流動,關(guān)注到并發(fā)問題中的數(shù)據(jù)流動,把流動的數(shù)據(jù)放到channel中,就能使用channel解決這個并發(fā)
問題,而且使用channel是線程安全的并且不會有數(shù)據(jù)沖突,比鎖好用多了
缺點:不太適應(yīng)同步太復(fù)雜的場景,比如多協(xié)程的同步等待問題,而且存在死鎖問題
分類
channel類型:無緩沖和緩沖類型
channel有兩種形式的,一種是無緩沖的,一個線程向這個channel發(fā)送了消息后,會阻塞當(dāng)前的這個線程,知道其他線程去接收這個channel的消息。無緩沖的形式如下:
intChan := make(chan int)
帶緩沖的channel,是可以指定緩沖的消息數(shù)量,當(dāng)消息數(shù)量小于指定值時,不會出現(xiàn)阻塞,超過之后才會阻塞,需要等待其他線程去接收channel處理,帶緩沖的形式如下:
//3為緩沖數(shù)量
intChan := make(chan int, 3)
舉例
type Person struct {
Name string
Age uint8
Address Addr
}
type Addr struct {
city string
district string
}
/*
測試channel傳輸復(fù)雜的Struct數(shù)據(jù)
*/
func testTranslateStruct() {
personChan := make(chan Person, 1)
person := Person{"xiaoming", 10, Addr{"shenzhen", "longgang"}}
personChan <- person
person.Address = Addr{"guangzhou", "huadu"}
fmt.Printf("src person : %+v \n", person)
newPerson := <-personChan
fmt.Printf("new person : %+v \n", newPerson)
}
在實際應(yīng)用過程中,等待channel 結(jié)束信號的過程可能不是無期限的,一般會伴隨一個timer,超時時間如下面所示:
/*
檢查channel讀寫超時,并做超時的處理
*/
func testTimeout() {
g := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case v := <-g:
fmt.Println(v)
case <-time.After(time.Second * time.Duration(3)):
quit <- true
fmt.Println("超時,通知主線程退出")
return
}
}
}()
for i := 0; i < 3; i++ {
g <- i
}
<-quit
fmt.Println("收到退出通知,主線程退出")
}
Mutex擁有Lock、Unlock兩個方法,主要的實現(xiàn)思想都體現(xiàn)在Lock函數(shù)中。
Lock執(zhí)行時,分三種情況:
無沖突 通過CAS操作把當(dāng)前狀態(tài)設(shè)置為加鎖狀態(tài);
有沖突 開始自旋,并等待鎖釋放,如果其他Goroutine在這段時間內(nèi)釋放了該鎖, 直接獲得該鎖;如果沒有釋放,進(jìn)入3;
有沖突,且已經(jīng)過了自旋階段 通過調(diào)用semacquire函數(shù)來讓當(dāng)前Goroutine進(jìn)入等待狀態(tài)。
無沖突時是最簡單的情況;有沖突時,首先進(jìn)行自旋,是從效率方面考慮的, 因為大多數(shù)的Mutex保護(hù)的代碼段都很短,經(jīng)過短暫的自旋就可以獲得;如果自旋等待無果,就只好通過信號量來讓當(dāng)前 Goroutine進(jìn)入等待了。
Channel在某些同步場景下,使用略顯復(fù)雜,不管是使用多個channel還是使用channel數(shù)組,如下:
func coordinateWithChan() {
sign := make(chan struct{}, 2)
num := int32(0)
fmt.Printf("The number: %d [with chan struct{}]\n", num)
max := int32(10)
go addNum(&num, 1, max, func() {
sign <- struct{}{}
})
go addNum(&num, 2, max, func() {
sign <- struct{}{}
})
<-sign
<-sign
}
所以Sync.waitGroup 就顯得更為優(yōu)雅,Sync.waitGroup 用來等待一組goroutines的結(jié)束,在主Goroutine里聲明,并且設(shè)置要等待的goroutine的個數(shù),每個goroutine執(zhí)行完成之后調(diào)用 Done,最后在主Goroutines 里Wait即可。類似于JAVA中的CountDownLatch或者循環(huán)屏障,并且Sync.waitGroup可以被重復(fù)使用,提供了如下API:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
但是Sync.waitGroup的使用需要遵循一些規(guī)則,避免拋出Panic:
a. 錯誤調(diào)用Done方法, 導(dǎo)致waitGroup內(nèi)部計數(shù)值出現(xiàn)負(fù)數(shù)的情況
b. 錯誤的調(diào)用Add方法,在waitGroup內(nèi)部計數(shù)值到達(dá)0的時候,Add方法被調(diào)用,導(dǎo)致應(yīng)該被喚起的goroutine沒有被喚起,就開始了新的一輪計數(shù)周期
所以在調(diào)用的時候,就要遵循一下原則:
先統(tǒng)一Add,再并發(fā)Done,最后Wait
Sync.once實現(xiàn)方式是內(nèi)部包含一個int32位的標(biāo)志,用來判斷方式是否被執(zhí)行過,標(biāo)志值更改的時機為方法執(zhí)行完之后,當(dāng)有多個goroutine進(jìn)行調(diào)用的時候,使用double-check方式進(jìn)行驗證,首先在在沒有同步方式的情況下,進(jìn)行標(biāo)志值的判定,為0則競爭獲取mutex鎖,進(jìn)入臨界區(qū)內(nèi),此時會在此進(jìn)行標(biāo)志值的判斷,確保方法真的被執(zhí)行一次。double-check第一次是為了更快的進(jìn)行判斷,但是存在錯誤的情況,第二次check是為了正確的確定標(biāo)志值此時的狀態(tài)。
使用:
func main() {
var once sync.Once
onceBody := func() {
time.Sleep(3e9)
fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
j := i
go func(int) {
once.Do(onceBody)
fmt.Println(j)
done <- true
}(j)
}
//給一部分時間保證能夠輸出完整【方法一】
//for i := 0; i < 10; i++ {
// <-done
//}
//給一部分時間保證能夠輸出完整【方法二】
<-done
time.Sleep(3e9)
}
場景
當(dāng)需要進(jìn)行多批次的計算任務(wù)同步,或者需要一對多的協(xié)作流程的時候
使用舉例
func coordinateWithContext() {
total := 12
var num int32
fmt.Printf("The number: %d [with context.Context]\n", num)
cxt, cancelFunc := context.WithCancel(context.Background())
for i := 1; i <= total; i++ {
go addNum(&num, i, func() {
if atomic.LoadInt32(&num) == int32(total) {
cancelFunc()
}
})
}
<-cxt.Done()
fmt.Println("End.")
}
注意事項
a.如何生成自己的context
通過WithCancel、WithDeadline、WithTimeout和WithValue四個方法從context.Background中派生出自己的子context
注意context.background這個上下文根節(jié)點僅僅是一個最基本的支點,它不提供任何額外的功能,也就是說,它既不可以被撤銷(cancel),也不能攜帶任何數(shù)據(jù),在使用是必須通過以上4種方法派生出自己的context
b.子context是會繼承父context的值
c.撤銷消息的傳播
撤銷消息會按照深度遍歷的方式傳播給子context(注意因為多routine調(diào)用的原因,最終的撤銷順序可能不會是深度遍歷的順序)
,在遍歷的過程中,通過WithCancel、WithDeadline、WithTimeout派生的context會被撤銷,但是通過WithValue方法派生的context不會被撤銷
我們調(diào)用sync/atomic中的幾個函數(shù)可以對幾種簡單的類型進(jìn)行原子操作。這些類型包括int32,int64,uint32,uint64,uintptr,unsafe.Pointer,共6個。這些函數(shù)的原子操作共有5種:增或減,比較并交換、載入、存儲和交換它們提供了不同的功能,切使用的場景也有區(qū)別。
增或減
顧名思義,原子增或減即可實現(xiàn)對被操作值的增大或減少。因此該操作只能操作數(shù)值類型。
被用于進(jìn)行增或減的原子操作都是以“Add”為前綴,并后面跟針對具體類型的名稱。
//方法源碼
func AddUint32(addr *uint32, delta uint32) (new uint32)
增
栗子:(在原來的基礎(chǔ)上加n)
atomic.AddUint32(&addr,n)
減
栗子:(在原來的基礎(chǔ)上加n(n為負(fù)數(shù)))
atomic.AddUint32(*addr,uint32(int32(n)))
//或
atomic.AddUint32(&addr,^uint32(-n-1))
比較并交換
比較并交換----Compare And Swap 簡稱CAS
他是假設(shè)被操作的值未曾被改變(即與舊值相等),并一旦確定這個假設(shè)的真實性就立即進(jìn)行值替換
如果想安全的并發(fā)一些類型的值,我們總是應(yīng)該優(yōu)先使用CAS
//方法源碼
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
栗子:(如果addr和old相同,就用new代替addr)
ok:=atomic.CompareAndSwapInt32(&addr,old,new)
載入
如果一個寫操作未完成,有一個讀操作就已經(jīng)發(fā)生了,這樣讀操作使很糟糕的。
為了原子的讀取某個值sync/atomic代碼包同樣為我們提供了一系列的函數(shù)。這些函數(shù)都以"Load"為前綴,意為載入。
//方法源碼
func LoadInt32(addr *int32) (val int32)
栗子
fun addValue(delta int32){
for{
v:=atomic.LoadInt32(&addr)
if atomic.CompareAndSwapInt32(&v,addr,(delta+v)){
break;
}
}
}
存儲
與讀操作對應(yīng)的是寫入操作,sync/atomic也提供了與原子的值載入函數(shù)相對應(yīng)的原子的值存儲函數(shù)。這些函數(shù)的名稱均以“Store”為前綴
在原子的存儲某個值的過程中,任何cpu都不會進(jìn)行針對進(jìn)行同一個值的讀或?qū)懖僮鳌H绻覀儼阉嗅槍Υ酥档膶懖僮鞫几臑樵硬僮鳎敲淳筒粫霈F(xiàn)針對此值的讀操作讀操作因被并發(fā)的進(jìn)行而讀到修改了一半的情況。
原子操作總會成功,因為他不必關(guān)心被操作值的舊值是什么。
//方法源碼
func StoreInt32(addr *int32, val int32)
栗子
atomic.StoreInt32(被操作值的指針,新值)
atomic.StoreInt32(&value,newaddr)
交換
原子交換操作,這類函數(shù)的名稱都以“Swap”為前綴。
與CAS不同,交換操作直接賦予新值,不管舊值。
會返回舊值
//方法源碼
func SwapInt32(addr *int32, new int32) (old int32)
栗子
atomic.SwapInt32(被操作值的指針,新值)(返回舊值)
oldval:=atomic.StoreInt32(&value,newaddr)
1. 什么是Sync包?
Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.
Values containing the types defined in this package should not be copied.
這句話大意是說:
Sync包同步提供基本的同步原語,如互斥鎖。 除了Once和WaitGroup類型之外,大多數(shù)類型都是供低級庫例程使用的。 通過Channel和溝通可以更好地完成更高級別的同步。并且此包中的值在使用過后不要拷貝。
從描述中可以看到的是,golang 并不推薦這個包中的大多數(shù)并發(fā)控制方法,但還是提供了相關(guān)方法,主要原因是golang中提倡以共享內(nèi)存的方式來通信:
不要以共享內(nèi)存的方式來通信,作為替代,我們應(yīng)該以通信的手段來共享內(nèi)存
共享內(nèi)存的方式使得多線程中的通信變得簡單,但是在并發(fā)的安全性控制上將變得異常繁瑣。
正確性不是我們唯一想要的,我們想要的還有系統(tǒng)的可伸縮性,以及可理解性,我覺得這點非常重要,比如現(xiàn)在廣泛使用的Raft算法。
2. 包中的Type
包中主要有: Locker, Cond, Map, Mutex, Once, Pool,
RWMutex, WaitGroup
type Locker interface {
Lock()
Unlock()
}
type Cond struct {
// L is held while observing or changing the condition
L Locker
}
3. 什么是鎖,為什么需要鎖?
鎖是sync包中的核心,他主要有兩個方法,加鎖和解鎖。
在單線程運行的時候程序是順序執(zhí)行的,程序?qū)?shù)據(jù)的訪問也是:
讀取=> 一頓操作(加減乘除之類的) => 寫回原地址
但是一旦程序中進(jìn)行了并發(fā)編程,也就是說,某一個函數(shù)可能同時被不同的線程執(zhí)行的時候,以時間為維度會發(fā)生以下情況:

可以看到的是,A地址的數(shù)字被執(zhí)行了兩次自增,若A=5,我們在執(zhí)行完成后預(yù)期的A值是7,但是在這種情況下我們得到的A卻是6,bug了~
還有很多類似的并發(fā)錯誤,所以才有鎖的引入。若是我們在線程2讀取A的值的時候?qū)進(jìn)行加鎖,讓線程2等待,線程1執(zhí)行完成之后在執(zhí)行線程2,這樣就能夠保證數(shù)據(jù)的正確性。但是正確性不是我們唯一想要的。
4 寫更優(yōu)雅的代碼
在很多語言中我們經(jīng)常為了保證數(shù)據(jù)安全正確,會在并發(fā)的時候?qū)?shù)據(jù)加鎖
Lock()
doSomething()
Unlock()
Golang在此包中也提供了相關(guān)的鎖,但是標(biāo)明了"most are intended for use by low-level library routines" 所以我這里只對 Once and WaitGroup types做簡述。
5.Once 對象
Once 是一個可以被多次調(diào)用但是只執(zhí)行一次,若每次調(diào)用Do時傳入?yún)?shù)f不同,但是只有第一個才會被執(zhí)行。
func (o *Once) Do(f func())
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
如果你執(zhí)行這段代碼會發(fā)現(xiàn),雖然調(diào)用了10次,但是只執(zhí)行了1次。BTW:這個東西可以用來寫單例。
6. WaitGroup
下面是個官方的例子:
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.somestupidname.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
7. 簡述
Golang中高級的并發(fā)可以通過channel來實現(xiàn),這是golang所倡導(dǎo)的,但是go也提供了鎖等先關(guān)操作。
以上就是“go語言同步機制是什么及怎么實現(xiàn)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
分享標(biāo)題:go語言同步機制是什么及怎么實現(xiàn)
文章URL:http://www.chinadenli.net/article36/geiepg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)建站、面包屑導(dǎo)航、小程序開發(fā)、網(wǎng)站設(shè)計公司、外貿(mào)網(wǎng)站建設(shè)、網(wǎng)站營銷
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)