當多個執行緒或goroutine訪問和操作相同的數據,而其最終的操作結果取決於執行的時序,可能導致不可預期或不一致的結果。
想像一下,兩位顧客在同一時間嘗試在線上商店購買僅剩一件的商品。由於系統沒有同時處理這兩個請求的機制,所以它告訴兩位顧客他們都成功地買到了那件商品。然而,實際上店家只有一件商品,這就是一個競態條件的簡單例子。
在Go中,當兩個或多個goroutines同時訪問同一記憶體位置而至少有一個寫入時,就可能會發生這種情況。
例如以下程式碼
package main
import (
"fmt"
"sync"
)
var counter int
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
// 以下的兩行程式碼是非原子操作,這會導致race condition。
// 當多個goroutine同時執行這些指令時,它們可能會讀取同一個`counter`值,
// 然後都進行加1的操作,這導致`counter`的增加少於1000。
localCounter := counter // 讀取`counter`的當前值
localCounter++ // 對本地的變數進行加1操作
counter = localCounter // 更新全局的`counter`變數
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter) // 這裡的輸出可能小於1000,因為發生了race condition
}
原子性參考於操作是不可分割的。這意味著當一個操作開始時,它將不會被其他的操作打斷,直到它完成為止。在我們前面的線上商店的例子中,如果購買操作是原子的,那麼當一個顧客嘗試購買最後一件商品時,另一個顧客的請求將會等待,直到第一個操作完成。
Go提供了一個sync/atomic包,這個包提供了基本的原子記憶體操作。使用這些原子操作可以確保記憶體更新的完整性,從而避免race condition。
不過,在Go中最常見的方法是使用sync.Mutex或sync.RWMutex。當goroutine想要訪問特定的數據時,它會先鎖定(lock)它。只有當鎖被釋放時,其他的goroutines才能訪問那段數據。
Memory Access Synchronization是關於當多個執行緒或goroutines訪問和修改共享資源時,確保數據完整性和一致性的過程。在多執行緒的程式中,如果沒有正確同步記憶體訪問,可能會導致不可預期的結果,這就是所謂的競態條件。
sync.Mutex
是Go提供的一種基本的鎖機制,用於確保同一時刻只有一個goroutine能夠訪問某段程式碼或資源,從而達到Memory Access Synchronization的目的。在上述程式中,我們使用mu.Lock()
和mu.Unlock()
來確保對counter
的原子性操作,從而避免race condition。
第一個範例可以修改如下
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex // 增加一個mutex來保護`counter`
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
// 使用mu.Lock()和mu.Unlock()來確保counter的原子性操作
mu.Lock() // 在更改`counter`之前鎖定
// 由於我們已經鎖定了對`counter`的訪問,現在只有一個goroutine可以同時修改它,
// 這確保了Memory Access Synchronization。
counter++
mu.Unlock() // 更新完`counter`後解鎖
wg.Done()
}()
}
wg.Wait()
fmt.Println(counter) // 此時,輸出應該確實為1000
}