在Golang中是鎖或Channel還是Atomic


  與其他編程語言一樣在並發環境下如不對多個goroutine(線程)訪問或修改的共享資源元素的進行控制,讓進入臨界區的對象互斥。就可能會出現數據異常情況;
  一個非線程安全對象如下,如不對Id的訪問進行控制,多個goroutine進行更新Id字段是就會出現數據不一致的情況,如下示例:

type Conf struct {
  Id int32
}
func(c *Conf)update(n int32){
  c.Id +=n
}

  啟動100個goroutine用於更新對象c中的Id字段值,此時由於出現多個協程同時進入臨界區同時對Id變量進行修改。導致對象c中的Id字段值出現了不可預知的情況。此時程序輸出的結果可能是:98、93、95等;

func main() {
 var c=&Conf{}
 for i := 0; i<100;i++  {
	go func(n int) {
		//模擬停頓
		time.Sleep(1*time.Millisecond)
		c.update(1)
	}(i)
 }
time.Sleep(10*time.Second)
fmt.Println(c.Id)
}

  下面分別使用鎖與channel對臨界區進行並發控制,使得輸出得到正常的結果,並簡單對比兩者的性能;

使用鎖

  現在在結構體Conf中添加一個讀寫鎖變量sync.RWMutex,此時Conf繼承了RWMutex中所有的方法字段等。通過此對象就可以對Id變量的訪問進行加鎖, struct變為如下:

type Conf struct {
  Id int32
  sync.RWMutex
}

定義一個新方法,此方法使用了鎖對臨界區訪問進行了並發控制:

func(c *Conf) updateOfLock(n int32){
   c.Lock()
   defer c.Unlock()
   c.Id +=n
}

  此時程序總是能夠輸出正確的結果:100

func main() {
var c=&Conf{}
for i := 0; i<100;i++  {
	go func(n int) {
		//模擬停頓
		time.Sleep(1*time.Millisecond)
		c.updateOfLock(1)
	}(i)
}
time.Sleep(10*time.Second)
fmt.Println(c.Id)
}

使用channel

  用channel控制臨界資源的訪問,原理也非常簡單,創建一個長度為1的channel變量,進去臨界區前往channel中寫入一個值,由於長度為1,此時別的協程將無法往channel中寫入值堵塞在此處channel上,前一個協程訪問完臨界區后從channel讀取值,此后別的協程可再此往channel中寫入值,從而能夠進入臨界區,結構體修改如下;

type Conf struct {
   Id int32
   lockChan chan bool
}
func (c *Conf) updateOfChannel(n int32) {
   c.lockChan<-true
   defer func() {<-c.lockChan}()
   c.Id +=n
}

  使用channel進行並發控制,需要注意取出lockChan中的值,此處使用了defer用於控制channel值的釋放。

 func main() {
    var c=&Conf{lockChan: make(chan bool,1)}
     for i := 0; i<100;i++  {
	go func(n int) {
		//模擬停頓
		time.Sleep(1*time.Millisecond)
		c.updateOfChannel(1)
	}(i)
     }
time.Sleep(10*time.Second)
fmt.Println(c.Id)
}

原子變量

  無需修改Conf結構體,直接調用atomic的相關方法即可,方法如下,此時也是能夠得到正確的值;

  func (c *Conf) updateOfAtomic(n int32) {
    atomic.AddInt32(&c.Id,n)
  }

性能簡單對比

  通過跑Golang基准測試看看鎖、原子變量、channel三者的性能如何,毫無疑問原子變量肯定是性能最好的,這是對底層的CAS操作連臨界區都沒有產生;這里主要對比鎖與channel性能,三個測試函數都和下函數類似;

 func BenchmarkAtomicTest(b *testing.B) {
    var c=&Conf{}
     for i := 0; i<b.N;i++  {
       c.updateOfAtomic(1)
    }
}

  此時可以看到毫無懸念的atomic原子操作的方式性能最高,比其他兩種低一個數量級,鎖的方式比channel時間快了一倍,每次平均執行時間只需44.45納秒,而channel每次需要84.12ns納秒;當然此處的只是簡單對比;

  能用atomic時肯定是用atomic,至於鎖和channel選哪個,還是要看應用場景、業務邏輯、可維護性等,總體來說兩者性能差別不算太大,如果不太熟悉channel在過於復雜的業務邏輯中channel或許可讀性會降低;但golang中的兩個核心就是goroutine和channel;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM