溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

go語(yǔ)言同步機(jī)制是什么及怎么實(shí)現(xiàn)

發(fā)布時(shí)間:2022-12-27 09:23:30 來(lái)源:億速云 閱讀:126 作者:iii 欄目:編程語(yǔ)言

今天小編給大家分享一下go語(yǔ)言同步機(jī)制是什么及怎么實(shí)現(xiàn)的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來(lái)了解一下吧。

go同步機(jī)制有:1、channel,著重并發(fā)問(wèn)題中的數(shù)據(jù)流動(dòng),把流動(dòng)的數(shù)據(jù)放到channel中,就能使用channel解決這個(gè)并發(fā);2、Sync.Mutex,擁有Lock、Unlock兩個(gè)方法,主要實(shí)現(xiàn)思想體現(xiàn)在Lock函數(shù)中;3、Sync.waitGroup;4、Sync.Once;5、Sync.context;6、Sync.pool;7、atomic包,針對(duì)變量進(jìn)行操作。

Golang的提供的同步機(jī)制有sync模塊下的Mutex、WaitGroup以及語(yǔ)言自身提供的chan等。

1.channel

概述

Golang以如此明顯的方式告訴我們:。

優(yōu)點(diǎn):channel的核心是數(shù)據(jù)流動(dòng),關(guān)注到并發(fā)問(wèn)題中的數(shù)據(jù)流動(dòng),把流動(dòng)的數(shù)據(jù)放到channel中,就能使用channel解決這個(gè)并發(fā)

          問(wèn)題,而且使用channel是線程安全的并且不會(huì)有數(shù)據(jù)沖突,比鎖好用多了

缺點(diǎn):不太適應(yīng)同步太復(fù)雜的場(chǎng)景,比如多協(xié)程的同步等待問(wèn)題,而且存在死鎖問(wèn)題

分類

channel類型:無(wú)緩沖和緩沖類型
channel有兩種形式的,一種是無(wú)緩沖的,一個(gè)線程向這個(gè)channel發(fā)送了消息后,會(huì)阻塞當(dāng)前的這個(gè)線程,知道其他線程去接收這個(gè)channel的消息。無(wú)緩沖的形式如下:

intChan := make(chan int)
 
帶緩沖的channel,是可以指定緩沖的消息數(shù)量,當(dāng)消息數(shù)量小于指定值時(shí),不會(huì)出現(xiàn)阻塞,超過(guò)之后才會(huì)阻塞,需要等待其他線程去接收channel處理,帶緩沖的形式如下:
 
//3為緩沖數(shù)量
intChan := make(chan int, 3)

舉例

 type Person struct {
	Name    string
	Age     uint8
	Address Addr
}
 
type Addr struct {
	city     string
	district string
}
 
/*
測(cè)試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)
}

在實(shí)際應(yīng)用過(guò)程中,等待channel 結(jié)束信號(hào)的過(guò)程可能不是無(wú)期限的,一般會(huì)伴隨一個(gè)timer,超時(shí)時(shí)間如下面所示:

/*
檢查channel讀寫(xiě)超時(shí),并做超時(shí)的處理
 */
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("超時(shí),通知主線程退出")
				return
			}
		}
	}()
 
	for i := 0; i < 3; i++ {
		g <- i
	}
 
	<-quit
	fmt.Println("收到退出通知,主線程退出")
}

2.Sync.Mutex

Mutex擁有Lock、Unlock兩個(gè)方法,主要的實(shí)現(xiàn)思想都體現(xiàn)在Lock函數(shù)中。

Lock執(zhí)行時(shí),分三種情況:

  • 無(wú)沖突 通過(guò)CAS操作把當(dāng)前狀態(tài)設(shè)置為加鎖狀態(tài);

  • 有沖突 開(kāi)始自旋,并等待鎖釋放,如果其他Goroutine在這段時(shí)間內(nèi)釋放了該鎖, 直接獲得該鎖;如果沒(méi)有釋放,進(jìn)入3;

  • 有沖突,且已經(jīng)過(guò)了自旋階段 通過(guò)調(diào)用semacquire函數(shù)來(lái)讓當(dāng)前Goroutine進(jìn)入等待狀態(tài)。

無(wú)沖突時(shí)是最簡(jiǎn)單的情況;有沖突時(shí),首先進(jìn)行自旋,是從效率方面考慮的, 因?yàn)榇蠖鄶?shù)的Mutex保護(hù)的代碼段都很短,經(jīng)過(guò)短暫的自旋就可以獲得;如果自旋等待無(wú)果,就只好通過(guò)信號(hào)量來(lái)讓當(dāng)前 Goroutine進(jìn)入等待了。

3. Sync.waitGroup

Channel在某些同步場(chǎng)景下,使用略顯復(fù)雜,不管是使用多個(gè)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 用來(lái)等待一組goroutines的結(jié)束,在主Goroutine里聲明,并且設(shè)置要等待的goroutine的個(gè)數(shù),每個(gè)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. 錯(cuò)誤調(diào)用Done方法, 導(dǎo)致waitGroup內(nèi)部計(jì)數(shù)值出現(xiàn)負(fù)數(shù)的情況

b. 錯(cuò)誤的調(diào)用Add方法,在waitGroup內(nèi)部計(jì)數(shù)值到達(dá)0的時(shí)候,Add方法被調(diào)用,導(dǎo)致應(yīng)該被喚起的goroutine沒(méi)有被喚起,就開(kāi)始了新的一輪計(jì)數(shù)周期

所以在調(diào)用的時(shí)候,就要遵循一下原則:

     先統(tǒng)一Add,再并發(fā)Done,最后Wait

4. Sync.Once

Sync.once實(shí)現(xiàn)方式是內(nèi)部包含一個(gè)int32位的標(biāo)志,用來(lái)判斷方式是否被執(zhí)行過(guò),標(biāo)志值更改的時(shí)機(jī)為方法執(zhí)行完之后,當(dāng)有多個(gè)goroutine進(jìn)行調(diào)用的時(shí)候,使用double-check方式進(jìn)行驗(yàn)證,首先在在沒(méi)有同步方式的情況下,進(jìn)行標(biāo)志值的判定,為0則競(jìng)爭(zhēng)獲取mutex鎖,進(jìn)入臨界區(qū)內(nèi),此時(shí)會(huì)在此進(jìn)行標(biāo)志值的判斷,確保方法真的被執(zhí)行一次。double-check第一次是為了更快的進(jìn)行判斷,但是存在錯(cuò)誤的情況,第二次check是為了正確的確定標(biāo)志值此時(shí)的狀態(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)
    }
    //給一部分時(shí)間保證能夠輸出完整【方法一】
    //for i := 0; i < 10; i++ {
    //    <-done
    //}

    //給一部分時(shí)間保證能夠輸出完整【方法二】
    <-done
    time.Sleep(3e9)
}

5. Sync.context

場(chǎng)景

當(dāng)需要進(jìn)行多批次的計(jì)算任務(wù)同步,或者需要一對(duì)多的協(xié)作流程的時(shí)候

使用舉例

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.")
}

注意事項(xiàng)

a.如何生成自己的context

通過(guò)WithCancel、WithDeadline、WithTimeout和WithValue四個(gè)方法從context.Background中派生出自己的子context

注意context.background這個(gè)上下文根節(jié)點(diǎn)僅僅是一個(gè)最基本的支點(diǎn),它不提供任何額外的功能,也就是說(shuō),它既不可以被撤銷(cancel),也不能攜帶任何數(shù)據(jù),在使用是必須通過(guò)以上4種方法派生出自己的context

b.子context是會(huì)繼承父context的值

c.撤銷消息的傳播

撤銷消息會(huì)按照深度遍歷的方式傳播給子context(注意因?yàn)槎鄏outine調(diào)用的原因,最終的撤銷順序可能不會(huì)是深度遍歷的順序)

,在遍歷的過(guò)程中,通過(guò)WithCancel、WithDeadline、WithTimeout派生的context會(huì)被撤銷,但是通過(guò)WithValue方法派生的context不會(huì)被撤銷

6. Sync.pool

7.atomic包,針對(duì)變量進(jìn)行操作

我們調(diào)用sync/atomic中的幾個(gè)函數(shù)可以對(duì)幾種簡(jiǎn)單的類型進(jìn)行原子操作。這些類型包括int32,int64,uint32,uint64,uintptr,unsafe.Pointer,共6個(gè)。這些函數(shù)的原子操作共有5種:增或減,比較并交換、載入、存儲(chǔ)和交換它們提供了不同的功能,切使用的場(chǎng)景也有區(qū)別。

增或減

   顧名思義,原子增或減即可實(shí)現(xiàn)對(duì)被操作值的增大或減少。因此該操作只能操作數(shù)值類型。

   被用于進(jìn)行增或減的原子操作都是以“Add”為前綴,并后面跟針對(duì)具體類型的名稱。

//方法源碼
func AddUint32(addr *uint32, delta uint32) (new uint32)

栗子:(在原來(lái)的基礎(chǔ)上加n)

atomic.AddUint32(&addr,n)

栗子:(在原來(lái)的基礎(chǔ)上加n(n為負(fù)數(shù)))

atomic.AddUint32(*addr,uint32(int32(n)))
//或
atomic.AddUint32(&addr,^uint32(-n-1))

比較并交換

   比較并交換----Compare And Swap 簡(jiǎn)稱CAS

   他是假設(shè)被操作的值未曾被改變(即與舊值相等),并一旦確定這個(gè)假設(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)

載入

   如果一個(gè)寫(xiě)操作未完成,有一個(gè)讀操作就已經(jīng)發(fā)生了,這樣讀操作使很糟糕的。

   為了原子的讀取某個(gè)值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;
        }
    }
}

存儲(chǔ)

   與讀操作對(duì)應(yīng)的是寫(xiě)入操作,sync/atomic也提供了與原子的值載入函數(shù)相對(duì)應(yīng)的原子的值存儲(chǔ)函數(shù)。這些函數(shù)的名稱均以“Store”為前綴

   在原子的存儲(chǔ)某個(gè)值的過(guò)程中,任何cpu都不會(huì)進(jìn)行針對(duì)進(jìn)行同一個(gè)值的讀或?qū)懖僮?。如果我們把所有針?duì)此值的寫(xiě)操作都改為原子操作,那么就不會(huì)出現(xiàn)針對(duì)此值的讀操作讀操作因被并發(fā)的進(jìn)行而讀到修改了一半的情況。

   原子操作總會(huì)成功,因?yàn)樗槐仃P(guān)心被操作值的舊值是什么。

//方法源碼
func StoreInt32(addr *int32, val int32)

栗子

atomic.StoreInt32(被操作值的指針,新值)
atomic.StoreInt32(&value,newaddr)

交換

   原子交換操作,這類函數(shù)的名稱都以“Swap”為前綴。

   與CAS不同,交換操作直接賦予新值,不管舊值。

   會(huì)返回舊值

//方法源碼
func SwapInt32(addr *int32, new int32) (old int32)

栗子

atomic.SwapInt32(被操作值的指針,新值)(返回舊值)
oldval:=atomic.StoreInt32(&value,newaddr)

擴(kuò)展知識(shí):Sync包簡(jiǎn)述

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.

這句話大意是說(shuō):
Sync包同步提供基本的同步原語(yǔ),如互斥鎖。 除了Once和WaitGroup類型之外,大多數(shù)類型都是供低級(jí)庫(kù)例程使用的。 通過(guò)Channel和溝通可以更好地完成更高級(jí)別的同步。并且此包中的值在使用過(guò)后不要拷貝。

從描述中可以看到的是,golang 并不推薦這個(gè)包中的大多數(shù)并發(fā)控制方法,但還是提供了相關(guān)方法,主要原因是golang中提倡以共享內(nèi)存的方式來(lái)通信:

不要以共享內(nèi)存的方式來(lái)通信,作為替代,我們應(yīng)該以通信的手段來(lái)共享內(nèi)存

共享內(nèi)存的方式使得多線程中的通信變得簡(jiǎn)單,但是在并發(fā)的安全性控制上將變得異常繁瑣。
正確性不是我們唯一想要的,我們想要的還有系統(tǒng)的可伸縮性,以及可理解性,我覺(jué)得這點(diǎn)非常重要,比如現(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包中的核心,他主要有兩個(gè)方法,加鎖和解鎖。
在單線程運(yùn)行的時(shí)候程序是順序執(zhí)行的,程序?qū)?shù)據(jù)的訪問(wèn)也是:
讀取 => 一頓操作(加減乘除之類的) => 寫(xiě)回原地址
但是一旦程序中進(jìn)行了并發(fā)編程,也就是說(shuō),某一個(gè)函數(shù)可能同時(shí)被不同的線程執(zhí)行的時(shí)候,以時(shí)間為維度會(huì)發(fā)生以下情況:

go語(yǔ)言同步機(jī)制是什么及怎么實(shí)現(xiàn)

可以看到的是,A地址的數(shù)字被執(zhí)行了兩次自增,若A=5,我們?cè)趫?zhí)行完成后預(yù)期的A值是7,但是在這種情況下我們得到的A卻是6,bug了~
還有很多類似的并發(fā)錯(cuò)誤,所以才有鎖的引入。若是我們?cè)诰€程2讀取A的值的時(shí)候?qū)進(jìn)行加鎖,讓線程2等待,線程1執(zhí)行完成之后在執(zhí)行線程2,這樣就能夠保證數(shù)據(jù)的正確性。但是正確性不是我們唯一想要的。

4 寫(xiě)更優(yōu)雅的代碼

在很多語(yǔ)言中我們經(jīng)常為了保證數(shù)據(jù)安全正確,會(huì)在并發(fā)的時(shí)候?qū)?shù)據(jù)加鎖

Lock()
doSomething()
Unlock()

Golang在此包中也提供了相關(guān)的鎖,但是標(biāo)明了"most are intended for use by low-level library routines" 所以我這里只對(duì) Once and WaitGroup types做簡(jiǎn)述。

5.Once 對(duì)象

Once 是一個(gè)可以被多次調(diào)用但是只執(zhí)行一次,若每次調(diào)用Do時(shí)傳入?yún)?shù)f不同,但是只有第一個(gè)才會(huì)被執(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í)行這段代碼會(huì)發(fā)現(xiàn),雖然調(diào)用了10次,但是只執(zhí)行了1次。BTW:這個(gè)東西可以用來(lái)寫(xiě)單例。

6. WaitGroup

下面是個(gè)官方的例子:

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. 簡(jiǎn)述

Golang中高級(jí)的并發(fā)可以通過(guò)channel來(lái)實(shí)現(xiàn),這是golang所倡導(dǎo)的,但是go也提供了鎖等先關(guān)操作。

以上就是“go語(yǔ)言同步機(jī)制是什么及怎么實(shí)現(xiàn)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI