天天看點

Golang中的同步工具原子操作詳解

作者:路多辛

前面幾篇文章介紹了Golang中互斥鎖、讀寫鎖、條件變量,雖然它們可以很好地協調對共享資源的通路,但并不能保證原子操作。

原子操作

原子操作是指一系列操作要麼全部執行成功,要麼全部執行失敗,不會有中間狀态。

鎖無法保證原子性是因為:

  • 在鎖保護的臨界區代碼執行期間,其他協程無法通路該代碼段,但是它們可以通路其他資源,可能會導緻原子操作失敗;
  • 鎖雖然能做到隻讓一個goroutine執行臨界區代碼,不被其他goroutine打擾,不過仍然可能被系統中斷(因為goroutine都是統一被runtime排程的,runtime會頻繁切換一個goroutine的運作狀态)

可以看出原子操作的粒度更細,它對單個變量的通路進行了原子化保證,在操作完成之前會阻塞其他并發操作。能夠保證原子性執行的隻有原子操作,原子操作在執行過程中是不允許被中斷的。在計算機底層,原子性是由CPU支援的,是以絕對有效。Golang中的原子操作是基于作業系統和CPU的,具體功能由标準庫中的sync/atomic包提供。

sync/atomic

sync/atomic包提供的原子操作有Add、Load、Store、Swap和CompareAndSwap,這些函數支援的資料類型有int32、int64、uint32、uint64、uintptr和unsafe包中的Pointer,不過,沒有提供針對unsafe.Pointer的Add方法。具體方法如下:

  • AddInt32/AddInt64/AddUint32/AddUint64/AddUintptr: 原子地将指定的值加到一個位址中的值上。
  • CompareAndSwapInt32/CompareAndSwapInt64/CompareAndSwapUint32/CompareAndSwapUint64/CompareAndSwapUintptr/CompareAndSwapPointer: 原子地比較一個指定類型位址中的值,如果該值和參數old比對,就在那個位址處存儲參數new。
  • SwapInt32/SwapInt64/SwapUint32/SwapUint64/SwapUintptr/SwapPointer: 原子地将值存儲在指定位址處,并傳回此位址的舊值。
  • LoadInt32/LoadInt64/LoadUint32/LoadUint64/LoadUintptr/LoadPointer: 原子地傳回指定位址中的值。
  • StoreInt32/StoreInt64/StoreUint32/StoreUint64/StoreUintptr/StorePointer: 原子地将指定值存儲到指定類型位址中。

此外,sync/atomic包還提供一個名稱為Value的類型,可以被用來存儲任意類型的值,結構如下:

type Value struct {
	v any
}           

使用方法和示例

使用原子操作可以用于計算需要在多個goroutine之間共享的計數器。例如,計算線上使用者數量、任務完成情況等等。

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var counter int64
	done := make(chan bool)

	for i := 0; i < 100; i++ {
		go func() {
			atomic.AddInt64(&counter, 1)
			done <- true
		}()
	}

	for i := 0; i < 100; i++ {
		<-done
	}

	fmt.Println(counter)
}           

首先聲明了一個int64類型的計數器變量counter,使用AddInt64原子操作對其進行遞增。通過使用AddInt64,確定了每個goroutine對其值的修改操作都能夠安全進行。

再看一個自旋鎖的示例:

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

func main() {
	sign := make(chan struct{}, 2)
	var num int32
	go func() {
		defer func() {
			sign <- struct{}{}
		}()
		for {
			// 定時增大num值
			time.Sleep(time.Millisecond * 500)
			newNum := atomic.AddInt32(&num, 2)
			fmt.Printf("num目前值為: %d\n", newNum)
			// 滿足條件後退出
			if newNum == 10 {
				break
			}
		}
	}()
	go func() {
		// 定時檢查num值,等于10則歸零
		defer func() {
			sign <- struct{}{}
		}()
		for {
			if atomic.CompareAndSwapInt32(&num, 10, 0) {
				fmt.Println("已将num歸零")
				break
			}
			time.Sleep(time.Millisecond * 500)
		}
	}()
	<-sign
	<-sign
}