您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“Golang提供了哪些原子性操作”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“Golang提供了哪些原子性操作”這篇文章吧。
Go語言通過內置包sync/atomic提供了對原子操作的支持,其提供的原子操作有以下幾大類:
增減,操作方法的命名方式為AddXXXType,保證對操作數進行原子的增減,支持的類型為int32、int64、uint32、uint64、uintptr,使用時以實際類型替換前面我說的XXXType就是對應的操作方法。
載入,保證了讀取到操作數前沒有其他任務對它進行變更,操作方法的命名方式為LoadXXXType,支持的類型除了基礎類型外還支持Pointer,也就是支持載入任何類型的指針。
存儲,有載入了就必然有存儲操作,這類操作的方法名以Store開頭,支持的類型跟載入操作支持的那些一樣。
比較并交換,也就是CAS (Compare And Swap),像Go的很多并發原語實現就是依賴的CAS操作,同樣是支持上面列的那些類型。
交換,這個簡單粗暴一些,不比較直接交換,這個操作很少會用。
平日里,在并發編程里,Go語言sync包里的同步原語Mutex是我們經常用來保證并發安全的,那么他跟atomic包里的這些操作有啥區別呢?在我看來他們在使用目的和底層實現上都不一樣:
使用目的:互斥鎖是用來保護一段邏輯,原子操作用于對一個變量的更新保護。
底層實現:Mutex由操作系統的調度器實現,而atomic包中的原子操作則由底層硬件指令直接提供支持,這些指令在執行的過程中是不允許中斷的,因此原子操作可以在lock-free的情況下保證并發安全,并且它的性能也能做到隨CPU個數的增多而線性擴展。
對于一個變量更新的保護,原子操作通常會更有效率,并且更能利用計算機多核的優勢。
比如下面這個,使用互斥鎖的并發計數器程序:
func mutexAdd() { var a int32 = 0 var wg sync.WaitGroup var mu sync.Mutex start := time.Now() for i := 0; i < 100000000; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() a += 1 mu.Unlock() }() } wg.Wait() timeSpends := time.Now().Sub(start).Nanoseconds() fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends) }
把Mutex改成用方法atomic.AddInt32(&a, 1)調用,在不加鎖的情況下仍然能確保對變量遞增的并發安全。
func AtomicAdd() { var a int32 = 0 var wg sync.WaitGroup start := time.Now() for i := 0; i < 1000000; i++ { wg.Add(1) go func() { defer wg.Done() atomic.AddInt32(&a, 1) }() } wg.Wait() timeSpends := time.Now().Sub(start).Nanoseconds() fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends) }
可以在本地運行以上這兩段代碼,可以觀察到計數器的結果都最后都是1000000,都是線程安全的。
需要注意的是,所有原子操作方法的被操作數形參必須是指針類型,通過指針變量可以獲取被操作數在內存中的地址,從而施加特殊的CPU指令,確保同一時間只有一個goroutine能夠進行操作。
上面的例子除了增加操作外我們還演示了載入操作,接下來我們來看一下CAS操作。
該操作簡稱CAS (Compare And Swap)。 這類操作的前綴為 CompareAndSwap :
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
該操作在進行交換前首先確保被操作數的值未被更改,即仍然保存著參數 old 所記錄的值,滿足此前提條件下才進行交換操作。CAS的做法類似操作數據庫時常見的樂觀鎖機制。
需要注意的是,當有大量的goroutine 對變量進行讀寫操作時,可能導致CAS操作無法成功,這時可以利用for循環多次嘗試。
上面我只列出了比較典型的int32和unsafe.Pointer類型的CAS方法,主要是想說除了讀數值類型進行比較交換,還支持對指針進行比較交換。
unsafe.Pointer提供了繞過Go語言指針類型限制的方法,unsafe指的并不是說不安全,而是說官方并不保證向后兼容。
// 定義一個struct類型P type P struct{ x, y, z int } // 執行類型P的指針 var pP *P func main() { // 定義一個執行unsafe.Pointer值的指針變量 var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP)) // Old pointer var sy P // 為了演示效果先將unsafe1設置成Old Pointer px := atomic.SwapPointer( unsafe1, unsafe.Pointer(&sy)) // 執行CAS操作,交換成功,結果返回true y := atomic.CompareAndSwapPointer( unsafe1, unsafe.Pointer(&sy), px) fmt.Println(y) }
上面的示例并不是在并發環境下進行的CAS,只是為了演示效果,先把被操作數設置成了Old Pointer。
其實Mutex的底層實現也是依賴原子操作中的CAS實現的,原子操作的atomic包相當于是sync包里的那些同步原語的實現依賴。
比如互斥鎖Mutex的結構里有一個state字段,其是表示鎖狀態的狀態位。
type Mutex struct { state int32 sema uint32 }
為了方便理解,我們在這里將它的狀態定義為0和1,0代表目前該鎖空閑,1代表已被加鎖,以下是sync.Mutex中Lock方法的部分實現代碼。
func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() }
在atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)中,m.state代表鎖的狀態,通過CAS方法,判斷鎖此時的狀態是否空閑(m.state==0),是,則對其加鎖(mutexLocked常量的值為1)。
atomic包里提供了一套Store開頭的方法,用來保證各種類型變量的并發寫安全,避免其他操作讀到了修改變量過程中的臟數據。
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) ...
這些操作方法的定義與上面介紹的那些操作的方法類似,我就不再演示怎么使用這些方法了。
值得一提的是如果你想要并發安全的設置一個結構體的多個字段,除了把結構體轉換為指針,通過StorePointer設置外,還可以使用atomic包后來引入的atomic.Value,它在底層為我們完成了從具體指針類型到unsafe.Pointer之間的轉換。
有了atomic.Value后,它使得我們可以不依賴于不保證兼容性的unsafe.Pointer類型,同時又能將任意數據類型的讀寫操作封裝成原子性操作(中間狀態對外不可見)。
atomic.Value類型對外暴露了兩個方法:
v.Store(c) - 寫操作,將原始的變量c存放到一個atomic.Value類型的v里。
c := v.Load() - 讀操作,從線程安全的v中讀取上一步存放的內容。
1.17 版本我看還增加了Swap和CompareAndSwap方法。
簡潔的接口使得它的使用也很簡單,只需將需要做并發保護的變量讀取和賦值操作用Load()和Store()代替就行了。
由于Load()返回的是一個interface{}類型,所以在使用前我們記得要先轉換成具體類型的值,再使用。下面是一個簡單的
例子演示atomic.Value的用法。
type Rectangle struct { length int width int } var rect atomic.Value func update(width, length int) { rectLocal := new(Rectangle) rectLocal.width = width rectLocal.length = length rect.Store(rectLocal) } func main() { wg := sync.WaitGroup{} wg.Add(10) // 10 個協程并發更新 for i := 0; i < 10; i++ { go func() { defer wg.Done() update(i, i+5) }() } wg.Wait() _r := rect.Load().(*Rectangle) fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length) }
你也可以試試,不用atomic.Value,直接給Rectange類型的指針變量賦值,看看在并發條件下,兩個字段的值是不是能跟預期的一樣變成10和15。
以上是“Golang提供了哪些原子性操作”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。