iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
0

在昨天我們已經看過了 golang 併發的方式,今天我們要來學習如何控制我們的 goroutines。首先我們先來看一下 race condition(或稱data race) 的定義與範例,接著我們在使用 channel 與 mutex lock 來處理這樣的問題。

Race Condition

A race condition or race hazard is the behavior of an electronics, software, or other system where the output is dependent on the sequence or timing of other uncontrollable events

Race condition 會發生,簡單說起來就是我們在同時間,對相同的變數進行了讀寫。試想我們都有一個變數 count = 0,若我們用併發的方式進行 count++,當然會發生兩個 goroutines 尚未等到對方寫回前就進行讀寫的狀況。

raceCondiction/main.go

package main

import "fmt"

func main() {
	exampleRace()
}

func exampleRace() {
	count := 0
	for i := 0; i < 10; i++ {
		go func() {
			count++
		}()
	}
	fmt.Println(count)
}

因發生 data race 導致結果錯誤

> go run basicGo/raceCondiction/main.go
8
> go run basicGo/raceCondiction/main.go
3
> go run basicGo/raceCondiction/main.go
7 

如果我們可以觀察 memory 的話,發生 race condiction 時應該會發生如下圖的狀況
data_race

Channel

為解決上面的窘境,我們可以使用 channel 來協助我們。 下面範例利用 no buffer channel 的 block 特性,在宣告 channel 時不指定 channel 大小,讓我們 blockCh 變數,只要收到一個資料便阻塞,以確保每次只有一個 goroutine 能進行 count++。

raceCondiction/main.go

package main

import (
	"fmt"
	"time"
)

func main() {
	// exampleRace()
	exampleChannel()
}

func exampleChannel() {
	count := 0

	//宣告一個 no buffer channel
	//利用其阻塞特性達成等待
	blockCh := make(chan bool)

	//waitting channel
	go func() {
		//blockCh 收到值才會++
		for {
			select {
			case <-blockCh:
				count++
			}
		}
	}()

	//concurrency
	for i := 0; i < 10; i++ {
		go func() {
			blockCh <- true
		}()
	}

	time.Sleep(100 * time.Millisecond)
	fmt.Println(count)
}

Mutex Lock

Mutex Lock 互斥鎖,使用上較為直覺,可在讀寫相同記憶體位置前先上鎖,避免該位置被另一線程讀寫,而造成資料競爭問題。但 mutex.Lock() 在 Unlock() 之前,再度 lock() 會導致程式異常 deadlock,這點是我們在使用 mutex 需特別注意的。

raceCondiction/main.go

package main

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

func main() {
	// exampleRace()
	// exampleChannel()
	exampleMutex()
}

func exampleMutex() {
	lock := sync.Mutex{}
	count := 0

	for i := 0; i < 10; i++ {
		go func() {
			//在對相同記憶體位置讀寫時,上鎖
			lock.Lock()
			count++
			lock.Unlock()
			//在對相同記憶體位置讀寫完成時,解鎖
		}()
	}

	time.Sleep(100 * time.Millisecond)
	fmt.Println(count)
}

何者較佳

今天我們分別用兩種手段,來處理 race condition 問題,其實這兩種方法並沒有實質的優劣之分,需依據不同的需求與條件使用。下一篇探討 mutex vs channel 的效能與試用情境,那麼我們明天見~


上一篇
Day9 Goroutine & Concurrency
下一篇
Day11 Mutex vs Channel
系列文
Go Distributed & Go Consistently30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
nagiMemo
iT邦新手 5 級 ‧ 2021-08-04 10:17:41

你好,我這邊使用-race參數來跑raceCondiction/main.go的範例,雖然結果多次執行會正確,但仍會被偵測出data race,這樣是正確的嗎?還是有用chan就可以忽視 data race?

go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c000122068 by main goroutine:
  main.exampleChannel()
      D:/go/src/demo-go/04/main.go:39 +0xfd
  main.main()
      D:/go/src/demo-go/04/main.go:10 +0x36

Previous write at 0x00c000122068 by goroutine 7:
  main.exampleChannel.func1()
      D:/go/src/demo-go/04/main.go:26 +0x69

Goroutine 7 (running) created at:
  main.exampleChannel()
      D:/go/src/demo-go/04/main.go:21 +0xa9
  main.main()
      D:/go/src/demo-go/04/main.go:10 +0x36
==================
10
Found 1 data race(s)
exit status 66

hi

會出現 data race 警示,是因為檢測認為 count 這個變數在你印出來的那個時間,上面的 goroutine 可能還在執行,雖然 count 本身在執行的最後結果會是正確的,但想要更完美的承接 count 可以配合 sync.WaitGroup,有使用 WaitGroup 也不用特別 sleep 了。

package main

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

func main() {
	// exampleRace()
	exampleChannel()
}

func exampleChannel() {
	count := 0
	wg := &sync.WaitGroup{}


	//宣告一個 no buffer channel
	//利用其阻塞特性達成等待
	blockCh := make(chan bool)

	//waitting channel
	go func() {
		//blockCh 收到值才會++
		for {
			select {
			case <-blockCh:
				count++
				wg.Done()
			}
		}
	}()

	//concurrency
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			blockCh <- true
		}()
	}
	wg.Wait()

	fmt.Println(count)
}
0
nagiMemo
iT邦新手 5 級 ‧ 2021-08-07 11:54:14

不好意思,race condition(或稱data race) 這句
在網路上看到這一個文章别混淆数据争用(data race) 和竞态条件(race condition)
https://blog.csdn.net/gg_18826075157/article/details/72582939
不知道這到底有什麼差別呢?

我要留言

立即登入留言