99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Go語言 其他并發(fā)同步技術(shù) - 如何使用sync標(biāo)準(zhǔn)庫包

2023-02-16 17:39 更新

通道用例大全一文中介紹了很多通過使用通道來實(shí)現(xiàn)并發(fā)同步的用例。 事實(shí)上,通道并不是Go支持的唯一的一種并發(fā)同步技術(shù)。而且對于一些特定的情形,通道并不是最有效和可讀性最高的同步技術(shù)。 本文下面將介紹?sync?標(biāo)準(zhǔn)庫包中提供的各種并發(fā)同步技術(shù)。相對于通道,這些技術(shù)對于某些情形更加適用。

?sync?標(biāo)準(zhǔn)庫包提供了一些用于實(shí)現(xiàn)并發(fā)同步的類型。這些類型適用于各種不同的內(nèi)存順序需求。 對于這些特定的需求,這些類型使用起來比通道效率更高,代碼實(shí)現(xiàn)更簡潔。

(請注意:為了避免各種異常行為,最好不要復(fù)制sync標(biāo)準(zhǔn)庫包中提供的類型的值。)

sync.WaitGroup(等待組)類型

每個sync.WaitGroup值在內(nèi)部維護(hù)著一個計(jì)數(shù),此計(jì)數(shù)的初始默認(rèn)值為零。

*sync.WaitGroup類型有三個方法Add(delta int)Done()Wait()。

對于一個可尋址的sync.WaitGroupwg,

  • 我們可以使用方法調(diào)用wg.Add(delta)來改變值wg維護(hù)的計(jì)數(shù)。
  • 方法調(diào)用wg.Done()wg.Add(-1)是完全等價的。
  • 如果一個wg.Add(delta)或者wg.Done()調(diào)用將wg維護(hù)的計(jì)數(shù)更改成一個負(fù)數(shù),一個恐慌將產(chǎn)生。
  • 當(dāng)一個協(xié)程調(diào)用了wg.Wait()時,
    • 如果此時wg維護(hù)的計(jì)數(shù)為零,則此wg.Wait()此操作為一個空操作(no-op);
    • 否則(計(jì)數(shù)為一個正整數(shù)),此協(xié)程將進(jìn)入阻塞狀態(tài)。 當(dāng)以后其它某個協(xié)程將此計(jì)數(shù)更改至0時(一般通過調(diào)用wg.Done()),此協(xié)程將重新進(jìn)入運(yùn)行狀態(tài)(即wg.Wait()將返回)。

請注意wg.Add(delta)wg.Done()wg.Wait()分別是(&wg).Add(delta)、(&wg).Done()(&wg).Wait()的簡寫形式。

一般,一個sync.WaitGroup值用來讓某個協(xié)程等待其它若干協(xié)程都先完成它們各自的任務(wù)。 一個例子:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano())

	const N = 5
	var values [N]int32

	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		i := i
		go func() {
			values[i] = 50 + rand.Int31n(50)
			fmt.Println("Done:", i)
			wg.Done() // <=> wg.Add(-1)
		}()
	}

	wg.Wait()
	// 所有的元素都保證被初始化了。
	fmt.Println("values:", values)
}

在此例中,主協(xié)程等待著直到其它5個協(xié)程已經(jīng)將各自負(fù)責(zé)的元素初始化完畢此會打印出各個元素值。 這里是一個可能的程序執(zhí)行輸出結(jié)果:

Done: 4
Done: 1
Done: 3
Done: 0
Done: 2
values: [71 89 50 62 60]

我們可以將上例中的Add方法調(diào)用拆分成多次調(diào)用:

...
	var wg sync.WaitGroup
	for i := 0; i < N; i++ {
		wg.Add(1) // 將被執(zhí)行5次
		i := i
		go func() {
			values[i] = 50 + rand.Int31n(50)
			wg.Done()
		}()
	}
...

一個*sync.WaitGroup值的Wait方法可以在多個協(xié)程中調(diào)用。 當(dāng)對應(yīng)的sync.WaitGroup值維護(hù)的計(jì)數(shù)降為0,這些協(xié)程都將得到一個(廣播)通知而結(jié)束阻塞狀態(tài)。

func main() {
	rand.Seed(time.Now().UnixNano())

	const N = 5
	var values [N]int32

	var wgA, wgB sync.WaitGroup
	wgA.Add(N)
	wgB.Add(1)

	for i := 0; i < N; i++ {
		i := i
		go func() {
			wgB.Wait() // 等待廣播通知
			log.Printf("values[%v]=%v \n", i, values[i])
			wgA.Done()
		}()
	}

	// 下面這個循環(huán)保證將在上面的任何一個
	// wg.Wait調(diào)用結(jié)束之前執(zhí)行。
	for i := 0; i < N; i++ {
		values[i] = 50 + rand.Int31n(50)
	}
	wgB.Done() // 發(fā)出一個廣播通知
	wgA.Wait()
}

一個WaitGroup可以在它的一個Wait方法返回之后被重用。 但是請注意,當(dāng)一個WaitGroup值維護(hù)的基數(shù)為零時,它的帶有正整數(shù)實(shí)參的Add方法調(diào)用不能和它的Wait方法調(diào)用并發(fā)運(yùn)行,否則將可能出現(xiàn)數(shù)據(jù)競爭。

sync.Once類型

每個*sync.Once值有一個Do(f func())方法。 此方法只有一個類型為func()的參數(shù)。

對一個可尋址的sync.Onceoo.Do()(即(&o).Do()的簡寫形式)方法調(diào)用可以在多個協(xié)程中被多次并發(fā)地執(zhí)行, 這些方法調(diào)用的實(shí)參應(yīng)該(但并不強(qiáng)制)為同一個函數(shù)值。 在這些方法調(diào)用中,有且只有一個調(diào)用的實(shí)參函數(shù)(值)將得到調(diào)用。 此被調(diào)用的實(shí)參函數(shù)保證在任何o.Do()方法調(diào)用返回之前退出。 換句話說,被調(diào)用的實(shí)參函數(shù)內(nèi)的代碼將在任何o.Do()方法返回調(diào)用之前被執(zhí)行。

一般來說,一個sync.Once值被用來確保一段代碼在一個并發(fā)程序中被執(zhí)行且僅被執(zhí)行一次。

一個例子:

package main

import (
	"log"
	"sync"
)

func main() {
	log.SetFlags(0)

	x := 0
	doSomething := func() {
		x++
		log.Println("Hello")
	}

	var wg sync.WaitGroup
	var once sync.Once
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			once.Do(doSomething)
			log.Println("world!")
		}()
	}

	wg.Wait()
	log.Println("x =", x) // x = 1
}

在此例中,Hello將僅被輸出一次,而world!將被輸出5次,并且Hello肯定在所有的5個world!之前輸出。

sync.Mutex(互斥鎖)和sync.RWMutex(讀寫鎖)類型

*sync.Mutex*sync.RWMutex類型都實(shí)現(xiàn)了sync.Locker接口類型。 所以這兩個類型都有兩個方法:Lock()Unlock(),用來保護(hù)一份數(shù)據(jù)不會被多個使用者同時讀取和修改。

除了Lock()Unlock()這兩個方法,*sync.RWMutex類型還有兩個另外的方法:RLock()RUnlock(),用來支持多個讀取者并發(fā)讀取一份數(shù)據(jù)但防止此份數(shù)據(jù)被某個數(shù)據(jù)寫入者和其它數(shù)據(jù)訪問者(包括讀取者和寫入者)同時使用。

(注意:這里的數(shù)據(jù)讀取者數(shù)據(jù)寫入者不應(yīng)該從字面上理解。有時候某些數(shù)據(jù)讀取者可能修改數(shù)據(jù),而有些數(shù)據(jù)寫入者可能只讀取數(shù)據(jù)。)

一個Mutex值常稱為一個互斥鎖。 一個Mutex零值為一個尚未加鎖的互斥鎖。 一個(可尋址的)Mutexm只有在未加鎖狀態(tài)時才能通過m.Lock()方法調(diào)用被成功加鎖。 換句話說,一旦m值被加了鎖(亦即某個m.Lock()方法調(diào)用成功返回), 一個新的加鎖試圖將導(dǎo)致當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài),直到此Mutex值被解鎖為止(通過m.Unlock()方法調(diào)用)。

注意:m.Lock()m.Unlock()分別是(&m).Lock()(&m).Unlock()的簡寫形式。

一個使用sync.Mutex的例子:

package main

import (
	"fmt"
	"runtime"
	"sync"
)

type Counter struct {
	m sync.Mutex
	n uint64
}

func (c *Counter) Value() uint64 {
	c.m.Lock()
	defer c.m.Unlock()
	return c.n
}

func (c *Counter) Increase(delta uint64) {
	c.m.Lock()
	c.n += delta
	c.m.Unlock()
}

func main() {
	var c Counter
	for i := 0; i < 100; i++ {
		go func() {
			for k := 0; k < 100; k++ {
				c.Increase(1)
			}
		}()
	}

	// 此循環(huán)僅為演示目的。
	for c.Value() < 10000 {
		runtime.Gosched()
	}
	fmt.Println(c.Value()) // 10000
}

在上面這個例子中,一個Counter值使用了一個Mutex字段來確保它的字段n永遠(yuǎn)不會被多個協(xié)程同時使用。

一個RWMutex值常稱為一個讀寫互斥鎖,它的內(nèi)部包含兩個鎖:一個寫鎖和一個讀鎖。 對于一個可尋址的RWMutexrwm,數(shù)據(jù)寫入者可以通過方法調(diào)用rwm.Lock()rwm加寫鎖,或者通過rwm.RLock()方法調(diào)用對rwm加讀鎖。 方法調(diào)用rwm.Unlock()rwm.RUnlock()用來解開rwm的寫鎖和讀鎖。 rwm的讀鎖維護(hù)著一個計(jì)數(shù)。當(dāng)rwm.RLock()調(diào)用成功時,此計(jì)數(shù)增1;當(dāng)rwm.Unlock()調(diào)用成功時,此計(jì)數(shù)減1; 一個零計(jì)數(shù)表示rwm的讀鎖處于未加鎖狀態(tài);反之,一個非零計(jì)數(shù)(肯定大于零)表示rwm的讀鎖處于加鎖狀態(tài)。

注意rwm.Lock()、rwm.Unlock()rwm.RLock()rwm.RUnlock()分別是(&rwm).Lock()、(&rwm).Unlock()(&rwm).RLock()(&rwm).RUnlock()的簡寫形式。

對于一個可尋址的RWMutexrwm,下列規(guī)則存在:

  • rwm的寫鎖只有在它的寫鎖和讀鎖都處于未加鎖狀態(tài)時才能被成功加鎖。 換句話說,rwm的寫鎖在任何時刻最多只能被一個數(shù)據(jù)寫入者成功加鎖,并且rwm的寫鎖和讀鎖不能同時處于加鎖狀態(tài)。
  • 當(dāng)rwm的寫鎖正處于加鎖狀態(tài)的時候,任何新的對之加寫鎖或者加讀鎖的操作試圖都將導(dǎo)致當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài),直到此寫鎖被解鎖,這樣的操作試圖才有機(jī)會成功。
  • 當(dāng)rwm的讀鎖正處于加鎖狀態(tài)的時候,新的加寫鎖的操作試圖將導(dǎo)致當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài)。 但是,一個新的加讀鎖的操作試圖將成功,只要此操作試圖發(fā)生在任何被阻塞的加寫鎖的操作試圖之前(見下一條規(guī)則)。 換句話說,一個讀寫互斥鎖的讀鎖可以同時被多個數(shù)據(jù)讀取者同時加鎖而持有。 當(dāng)rwm的讀鎖維護(hù)的計(jì)數(shù)清零時,讀鎖將返回未加鎖狀態(tài)。
  • 假設(shè)rwm的讀鎖正處于加鎖狀態(tài)的時候,為了防止后續(xù)數(shù)據(jù)寫入者沒有機(jī)會成功加寫鎖,后續(xù)發(fā)生在某個被阻塞的加寫鎖操作試圖之后的所有加讀鎖的試圖都將被阻塞。
  • 假設(shè)rwm的寫鎖正處于加鎖狀態(tài)的時候,(至少對于標(biāo)準(zhǔn)編譯器來說,)為了防止后續(xù)數(shù)據(jù)讀取者沒有機(jī)會成功加讀鎖,發(fā)生在此寫鎖下一次被解鎖之前的所有加讀鎖的試圖都將在此寫鎖下一次被解鎖之后肯定取得成功,即使所有這些加讀鎖的試圖發(fā)生在一些仍被阻塞的加寫鎖的試圖之后。

后兩條規(guī)則是為了確保數(shù)據(jù)讀取者和寫入者都有機(jī)會執(zhí)行它們的操作。

請注意:一個鎖并不會綁定到一個協(xié)程上,即一個鎖并不記錄哪個協(xié)程成功地加鎖了它。 換句話說,一個鎖的加鎖者和此鎖的解鎖者可以不是同一個協(xié)程,盡管在實(shí)踐中這種情況并不多見。

在上一個例子中,如果Value方法被十分頻繁調(diào)用而Increase方法并不頻繁被調(diào)用,則Counter類型的m字段的類型可以更改為sync.RWMutex,從而使得執(zhí)行效率更高,如下面的代碼所示。

...
type Counter struct {
	//m sync.Mutex
	m sync.RWMutex
	n uint64
}

func (c *Counter) Value() uint64 {
	//c.m.Lock()
	//defer c.m.Unlock()
	c.m.RLock()
	defer c.m.RUnlock()
	return c.n
}
...

sync.RWMutex值的另一個應(yīng)用場景是將一個寫任務(wù)分隔成若干小的寫任務(wù)。下一節(jié)中展示了一個這樣的例子。

根據(jù)上面列出的后兩條規(guī)則,下面這個程序最有可能輸出abdc。

package main

import (
	"fmt"
	"time"
	"sync"
)

func main() {
	var m sync.RWMutex
	go func() {
		m.RLock()
		fmt.Print("a")
		time.Sleep(time.Second)
		m.RUnlock()
	}()
	go func() {
		time.Sleep(time.Second * 1 / 4)
		m.Lock()
		fmt.Print("b")
		time.Sleep(time.Second)
		m.Unlock()
	}()
	go func() {
		time.Sleep(time.Second * 2 / 4)
		m.Lock()
		fmt.Print("c")
		m.Unlock()
	}()
	go func () {
		time.Sleep(time.Second * 3 / 4)
		m.RLock()
		fmt.Print("d")
		m.RUnlock()
	}()
	time.Sleep(time.Second * 3)
	fmt.Println()
}

請注意,上例這個程序僅僅是為了解釋和驗(yàn)證上面列出的讀寫鎖的后兩條加鎖規(guī)則。 此程序使用了time.Sleep調(diào)用來做協(xié)程間的同步。這種所謂的同步方法不應(yīng)該被使用在生產(chǎn)代碼中。

sync.Mutexsync.RWMutex值也可以用來實(shí)現(xiàn)通知,盡管這不是Go中最優(yōu)雅的方法來實(shí)現(xiàn)通知。 下面是一個使用了Mutex值來實(shí)現(xiàn)通知的例子。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var m sync.Mutex
	m.Lock()
	go func() {
		time.Sleep(time.Second)
		fmt.Println("Hi")
		m.Unlock() // 發(fā)出一個通知
	}()
	m.Lock() // 等待通知
	fmt.Println("Bye")
}

在此例中,Hi將確保在Bye之前打印出來。 關(guān)于sync.Mutexsync.RWMutex值相關(guān)的內(nèi)存順序保證,請閱讀Go中的內(nèi)存順序保證一文。

sync.Cond類型

sync.Cond類型提供了一種有效的方式來實(shí)現(xiàn)多個協(xié)程間的通知。

每個sync.Cond值擁有一個sync.Locker類型的名為L的字段。 此字段的具體值常常為一個*sync.Mutex值或者*sync.RWMutex值。

*sync.Cond類型有三個方法Wait()、Signal()Broadcast()

每個Cond值維護(hù)著一個先進(jìn)先出等待協(xié)程隊(duì)列。 對于一個可尋址的Condc

  • c.Wait()必須在c.L字段值的鎖處于加鎖狀態(tài)的時候調(diào)用;否則,c.Wait()調(diào)用將造成一個恐慌。 一個c.Wait()調(diào)用將
    1. 首先將當(dāng)前協(xié)程推入到c所維護(hù)的等待協(xié)程隊(duì)列;
    2. 然后調(diào)用c.L.Unlock()c.L的鎖解鎖;
    3. 然后使當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài);

      (當(dāng)前協(xié)程將被另一個協(xié)程通過c.Signal()c.Broadcast()調(diào)用喚醒而重新進(jìn)入運(yùn)行狀態(tài)。)

      一旦當(dāng)前協(xié)程重新進(jìn)入運(yùn)行狀態(tài),c.L.Lock()將被調(diào)用以試圖重新對c.L字段值的鎖加鎖。 此c.Wait()調(diào)用將在此試圖成功之后退出。
  • 一個c.Signal()調(diào)用將喚醒并移除c所維護(hù)的等待協(xié)程隊(duì)列中的第一個協(xié)程(如果此隊(duì)列不為空的話)。
  • 一個c.Broadcast()調(diào)用將喚醒并移除c所維護(hù)的等待協(xié)程隊(duì)列中的所有協(xié)程(如果此隊(duì)列不為空的話)。

請注意:c.Wait()、c.Signal()c.Broadcast()分別為(&c).Wait()、(&c).Signal()(&c).Broadcast()的簡寫形式。

c.Signal()c.Broadcast()調(diào)用常用來通知某個條件的狀態(tài)發(fā)生了變化。 一般說來,c.Wait()應(yīng)該在一個檢查某個條件是否已經(jīng)得到滿足的循環(huán)中調(diào)用。

下面是一個典型的sync.Cond用例。

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano())

	const N = 10
	var values [N]string

	cond := sync.NewCond(&sync.Mutex{})

	for i := 0; i < N; i++ {
		d := time.Second * time.Duration(rand.Intn(10)) / 10
		go func(i int) {
			time.Sleep(d) // 模擬一個工作負(fù)載
			cond.L.Lock()
			// 下面的修改必須在cond.L被鎖定的時候執(zhí)行
			values[i] = string('a' + i)
			cond.Broadcast() // 可以在cond.L被解鎖后發(fā)出通知
			cond.L.Unlock()
			// 上面的通知也可以在cond.L未鎖定的時候發(fā)出。
			//cond.Broadcast() // 上面的調(diào)用也可以放在這里
		}(i)
	}

	// 此函數(shù)必須在cond.L被鎖定的時候調(diào)用。
	checkCondition := func() bool {
		fmt.Println(values)
		for i := 0; i < N; i++ {
			if values[i] == "" {
				return false
			}
		}
		return true
	}

	cond.L.Lock()
	defer cond.L.Unlock()
	for !checkCondition() {
		cond.Wait() // 必須在cond.L被鎖定的時候調(diào)用
	}
}

一個可能的輸出:

[         ]
[     f    ]
[  c   f    ]
[  c   f  h  ]
[ b c   f  h  ]
[a b c   f  h  j]
[a b c   f g h i j]
[a b c  e f g h i j]
[a b c d e f g h i j]

因?yàn)樯侠兄挥幸粋€協(xié)程(主協(xié)程)在等待通知,所以其中的cond.Broadcast()調(diào)用也可以換為cond.Signal()。 如上例中的注釋所示,cond.Broadcast()cond.Signal()不必在cond.L的鎖處于加鎖狀態(tài)時調(diào)用。

為了防止數(shù)據(jù)競爭,對自定義條件的修改必須在cond.L的鎖處于加鎖狀態(tài)時才能執(zhí)行。 另外,checkCondition函數(shù)和cond.Wait方法也必須在cond.L的鎖處于加鎖狀態(tài)時才可被調(diào)用。

事實(shí)上,對于上面這個特定的例子,cond.L字段的也可以為一個*sync.RWMutex值。 對自定義條件的十個部分的修改可以在RWMutex值的讀鎖處于加鎖狀態(tài)時執(zhí)行。這十個修改可以并發(fā)進(jìn)行,因?yàn)樗鼈兪腔ゲ桓蓴_的。 如下面的代碼所示:

...
	cond := sync.NewCond(&sync.RWMutex{})
	cond.L.Lock()

	for i := 0; i < N; i++ {
		d := time.Second * time.Duration(rand.Intn(10)) / 10
		go func(i int) {
			time.Sleep(d)
			cond.L.(*sync.RWMutex).RLock()
			values[i] = string('a' + i)
			cond.L.(*sync.RWMutex).RUnlock()
			cond.Signal()
		}(i)
	}
...

在上面的代碼中,此sync.RWMutex值的用法有些不符常規(guī)。 它的讀鎖被一些修改數(shù)組元素的協(xié)程所加鎖并持有,而它的寫鎖被主協(xié)程加鎖持有用來讀取并檢查各個數(shù)組元素的值。

Cond值所表示的自定義條件可以是一個虛無。對于這種情況,此Cond值純粹被用來實(shí)現(xiàn)通知。 比如,下面這個程序?qū)⒋蛴〕?code>abc或者bac。

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	wg.Add(1)
	cond := sync.NewCond(&sync.Mutex{})
	cond.L.Lock()
	go func() {
		cond.L.Lock()
		go func() {
			cond.L.Lock()
			cond.Broadcast()
			cond.L.Unlock()
		}()
		cond.Wait()
		fmt.Print("a")
		cond.L.Unlock()
		wg.Done()
	}()
	cond.Wait()
	fmt.Print("b")
	cond.L.Unlock()
	wg.Wait()
	fmt.Println("c")
}

如果需要,多個sync.Cond值可以共享一個sync.Locker值。但是這種情形在實(shí)踐中并不多見。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號