go學習之- cas的理解

參考文章:
[CAS(Compare and Swap)算法介紹、缺陷和解決思路](
https://blog.csdn.net/q2878948/article/details/90105951
https://www.jianshu.com/p/c74c85db5129)

CAS(compare and swap)

  1. go中CAS操作具有原子性,在解決多線程操作共享變量安全上可以有效的減少使用鎖所帶來的開銷,但是這是使用cpu資源做交換的
  2. go中的Cas操作與java中類似,都是借用了CPU提供的原子性指令來實現。CAS操作修改共享變量時候不需要對共享變量加鎖,而是通過類似樂觀鎖的方式進行檢查,本質還是不斷的佔用CPU 資源換取加鎖帶來的開銷(比如上下文切換開銷)(參考文章:https://www.jianshu.com/p/4e61ed8e140a)

原子操作主要由硬件提供支持,鎖一般是由操作系統提供支持,比起直接使用鎖,使用CAS這個過程不需要形成臨界區和創建互斥量,所以會比使用鎖更加高效。

從硬件層面來實現原子操作,有兩種方式:

1、總線加鎖:因爲CPU和其他硬件的通信都是通過總線控制的,所以可以通過在總線加LOCK#鎖的方式實現原子操作,但這樣會阻塞其他硬件對CPU的訪問,開銷比較大。

2、緩存鎖定:頻繁使用的內存會被處理器放進高速緩存中,那麼原子操作就可以直接在處理器的高速緩存中進行而不需要使用總線鎖,主要依靠緩存一致性來保證其原子性。
————————————————
版權聲明:本文爲CSDN博主「菌菇」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_39920531/article/details/97646901

下面一個例子使用CAS來實現計數器,把這個例子理解了差不多就理解cas原理了:

package main

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

var (
	counter int32//計數器
	wg sync.WaitGroup //信號量
)

func main() {
	threadNum := 5 //1. 五個信號量
	wg.Add(threadNum) //2.開啓5個線程
	for i := 0; i < threadNum; i++ {
		go incCounter(i)
	}
	//3.等待子線程結束
	wg.Wait()
	fmt.Println(counter)
}

func incCounter(index int) {
	defer wg.Done()
	spinNum := 0
	for {
		//2.1原子操作
		old := counter
		ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
		if ok {
			break
		} else {
			spinNum++
		}
	}
	fmt.Printf("thread,%d,spinnum,%d\n",index,spinNum)
}

執行結果

thread,4,spinnum,0
thread,0,spinnum,0
thread,1,spinnum,0
thread,3,spinnum,0
thread,2,spinnum,0
5

如上代碼main線程首先創建了5個信號量,然後開啓五個線程執行incCounter方法

incCounter內部執行代碼2.1 使用cas操作遞增counter的值, atomic.CompareAndSwapInt32具有三個參數,第一個是變量的地址,第二個是變量當前值,第三個是要修改變量爲多少,該函數如果發現傳遞的old值等於當前變量的值,則使用第三個變量替換變量的值並返回true,否則返回false。

這裏之所以使用無限循環是因爲在高併發下每個線程執行CAS並不是每次都成功,失敗了的線程需要重寫獲取變量當前的值,然後重新執行CAS操作。讀者可以把線程數改爲10000或者更多會發現輸出thread,5329,spinnum,1其中1說明該線程嘗試了兩個CAS操作,第二次才成功。

CAS的缺陷

1.循環開銷大
可以看到,方法內部用不斷循環的方式實現修改。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。

2.只能保證一個共享變量的原子操作
需要對多個共享變量操作時,循環CAS就無法保證操作的原子性。
解決方法:可以把多個變量放在一個對象裏來進行CAS操作。

3.ABA問題
CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼CAS進行檢查的時候發現它的值沒有發生變化,但是實質上它已經發生了改變 。可能會造成數據的缺失

有篇文章:Go 的一個 CAS 操作使用場景可以看一看。

這個例子是在使用cas代替互斥鎖:(降低開銷)

package main

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

const (
	_CHAN_SIZE  = 10
	_GUARD_SIZE = 10

	_TEST_CNT = 32
)

type Obj struct {
	flag int64
	c    chan interface{}
}

func (obj *Obj) readLoop() error {
	counter := _TEST_CNT
	for {
		time.Sleep(5 * time.Millisecond)
		if len(obj.c) > _CHAN_SIZE {
			return errors.New(fmt.Sprintf("Chan overflow, len: %v.", len(obj.c)))
		} else if len(obj.c) > 0 {
			<-obj.c
			counter--
		}
		if counter <= 0 {
			return nil
		}
	}
}

func (obj *Obj) writeMsg(idx int, v interface{}) (err error) {
	for {
		if len(obj.c) < _CHAN_SIZE {
			obj.c <- v
			fmt.Printf("R(%v)+1 ", idx)
			return nil
		}
	}
}

func (obj *Obj) writeMsgWithCASCheck(idx int, v interface{}) (err error) {
	for {
		if atomic.CompareAndSwapInt64(&obj.flag, 0, 1) {
			if len(obj.c) < _CHAN_SIZE {
				obj.c <- v
				atomic.StoreInt64(&obj.flag, 0)
				fmt.Printf("R(%v)+1 ", idx)
				return nil
			} else {
				atomic.StoreInt64(&obj.flag, 0)
			}
		}
	}

	return nil
}

func main() {
	useCAS := false
	if len(os.Args) > 1 && os.Args[1] == "cas" {
		useCAS = true
	}
	routineCnt := 4
	tryCnt := _TEST_CNT / routineCnt
	var obj = &Obj{c: make(chan interface{}, _CHAN_SIZE+_GUARD_SIZE)}

	for idx := 0; idx < routineCnt; idx++ {
		go func(nameIdx int) {
			for tryIdx := 0; tryIdx < tryCnt; tryIdx++ {
				if useCAS {
					obj.writeMsgWithCASCheck(nameIdx, nil)
				} else {
					obj.writeMsg(nameIdx, nil)
				}
			}
		}(idx)
	}

	// fmt.Println(casObj.readLoop())
	fmt.Println(obj.readLoop())
	fmt.Println("quit.")
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章