GO 原子操作 atomic包

原子操作簡介

原子操作即是進行過程中不能被中斷的操作,針對某個值的原子操作在被進行的過程中,CPU絕不會再去進行其他的針對該值的操作。爲了實現這樣的嚴謹性,原子操作僅會由一個獨立的CPU指令代表和完成。原子操作是無鎖的,常常直接通過CPU指令直接實現。 事實上,其它同步技術的實現常常依賴於原子操作。

具體的原子操作在不同的操作系統中實現是不同的。比如在Intel的CPU架構機器上,主要是使用總線鎖的方式實現的。 大致的意思就是當一個CPU需要操作一個內存塊的時候,向總線發送一個LOCK信號,所有CPU收到這個信號後就不對這個內存塊進行操作了。 等待操作的CPU執行完操作後,發送UNLOCK信號,才結束。 在AMD的CPU架構機器上就是使用MESI一致性協議的方式來保證原子操作。 所以我們在看atomic源碼的時候,我們看到它針對不同的操作系統有不同彙編語言文件。

代碼中的加鎖操作因爲涉及內核態的上下文切換會比較耗時、代價比較高。

針對基本數據類型我們還可以使用原子操作來保證併發安全,

因爲原子操作是Go語言提供的方法它在用戶態就可以完成,因此性能比加鎖操作更好。

Go語言中原子操作由內置的標準庫sync/atomic提供。

包中的方法

方法分成這幾類:

  • AddXXX:操作一個數字類型,加上一個數字
  • LoadXXX:讀取一個值
  • CompareAndSwapXXX:比較並交換,大名鼎鼎的CAS操作
  • StoreXXX:寫入一個值
  • SwapXXX:寫入一個值,並且返回舊的值。 它和 CompareAndSwap 的區別在於它不關 心舊的值是什麼
  • unsafepointer 相關方法,不建議使用。難寫 也難讀,不到逼不得已不要去用,尤其是不要 爲了優化而故意用 unsafepoint

使用案例

package main

import "sync/atomic"

var value int32 = 0

func main() {
	// 要傳入 value 的指針
	// 把 value + 10
	atomic.AddInt32(&value, 10)
	nv := atomic.LoadInt32(&value)
	// 輸出10
	println(nv)
	// 如果之前的值是10,那麼就設置爲新的值 20
	swapped := atomic.CompareAndSwapInt32(&value, 10, 20)
	// 輸出 true
	println(swapped)

	// 如果之前的值是19,那麼就設置爲新的值 50
	// 顯然現在 value 是 20
	swapped = atomic.CompareAndSwapInt32(&value, 19, 50)
	// 輸出 false
	println(swapped)

	old := atomic.SwapInt32(&value, 40)
	// 應該是20,即原本的值
	println(old)
	// 輸出新的值,也就是交換後的值,40
	println(value)
}

image-20230204210147113

互斥鎖和原子操作的性能比較

package main

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

var x int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函數
func add() {
	// x = x + 1
	x++ // 等價於上面的操作
	wg.Done()
}

// 互斥鎖版加函數
func mutexAdd() {
	l.Lock()
	x++
	l.Unlock()
	wg.Done()
}

// 原子操作版加函數
func atomicAdd() {
	atomic.AddInt64(&x, 1)
	wg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10000; i++ {
		wg.Add(1)
		// go add() // 普通版add函數 不是併發安全的 9683 5.4649ms
		// go mutexAdd() // 加鎖版add函數 是併發安全的,但是加鎖性能開銷大 10000 6.0296ms
		go atomicAdd() // 原子操作版add函數 是併發安全,性能優於加鎖版 10000 5.5254ms
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(x)
	fmt.Println(end.Sub(start))
}

image-20230204210926017

原子操作與互斥鎖的區別

首先atomic操作的優勢是更輕量,比如CAS可以在不形成臨界區和創建互斥量的情況下完成併發安全的值替換操作。這可以大大的減少同步對程序性能的損耗。

原子操作也有劣勢。還是以CAS操作爲例,使用CAS操作的做法趨於樂觀,總是假設被操作值未曾被改變(即與舊值相等),並一旦確認這個假設的真實性就立即進行值替換,那麼在被操作值被頻繁變更的情況下,CAS操作並不那麼容易成功。而使用互斥鎖的做法則趨於悲觀,我們總假設會有併發的操作要修改被操作的值,並使用鎖將相關操作放入臨界區中加以保護。

下面是幾點區別:

  • 互斥鎖是一種數據結構,用來讓一個線程執行程序的關鍵部分,完成互斥的多個操作
  • 原子操作是無鎖的,常常直接通過CPU指令直接實現
  • 原子操作中的cas趨於樂觀鎖,CAS操作並不那麼容易成功,需要判斷,然後嘗試處理
  • 可以把互斥鎖理解爲悲觀鎖,共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程

atomic包提供了底層的原子性內存原語,這對於同步算法的實現很有用。這些函數一定要非常小心地使用,使用不當反而會增加系統資源的開銷,對於應用層來說,最好使用通道或sync包中提供的功能來完成同步操作。

針對atomic包的觀點在Google的郵件組裏也有很多討論,其中一個結論解釋是:

應避免使用該包裝。或者,閱讀C ++ 11標準的“原子操作”一章;如果您瞭解如何在C ++中安全地使用這些操作,那麼你纔能有安全地使用Go的sync/atomic包的能力。

有關樂觀鎖,悲觀鎖,CAS等解釋:

https://blog.csdn.net/weixin_50966947/article/details/124096601

https://www.jianshu.com/p/d2ac26ca6525

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章