Go併發編程之傳統同步—(3)原子操做

前言

以前文章中介紹的互斥鎖雖然可以保證同串行化,可是卻保證不了執行過程當中的中斷。
要麼成功、要麼失敗,沒有中斷的狀況,咱們叫它叫原子性,這種由硬件 CPU 提供支持的特性,是很是可靠的。git

百度百科上關於原子操做的介紹。github

原子操做

由 sync/atomic 包提供操做支持。編程

加法(add)

實現累加併發

func TestDemo1(t *testing.T) {
    var counter int64 = 0

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

    time.Sleep(2 * time.Second)
    log.Println("counter:", atomic.LoadInt64(&counter))
}

結果性能

=== RUN   TestDemo1
2020/10/11 00:24:56 counter: 100
--- PASS: TestDemo1 (2.00s)
PASS

減法(add)

對於作減法,是沒有直接提供的方法的,而 Add(-1)這種是不能對 uint 類型使用的,能夠經過補碼的方式實現學習

func TestDemo2(t *testing.T) {
    var counter uint64 = 100

    for i := 0; i < 100; i++ {
        go func() {
            atomic.AddUint64(&counter, ^uint64(-(-1)-1))
        }()
    }

    time.Sleep(2 * time.Second)
    log.Println("counter:", atomic.LoadUint64(&counter))
}

結果ui

=== RUN   TestDemo2
2020/10/11 00:32:05 counter: 0
--- PASS: TestDemo2 (2.00s)
PASS

比較並交換(compare and swap,簡稱 CAS)

併發編程中,在沒有使用互斥鎖的前提下,對共享數據先取出作判斷,再根據判斷的結果作後續操做,必然是會出問題的,使用 CAS 能夠避免這種問題。atom

func TestDemo3(t *testing.T) {
    var first int64 = 0

    for i := 1; i <= 10000; i++ {
        go func(i int) {
            if atomic.CompareAndSwapInt64(&first, 0, int64(i)) {
                log.Println("搶先運行的是 goroutine", i)
            }
        }(i)
    }

    time.Sleep(2 * time.Second)
    log.Println("num:", atomic.LoadInt64(&first))
}

結果code

=== RUN   TestDemo3
2020/10/11 00:42:10 搶先運行的是 goroutine 3
2020/10/11 00:42:12 num: 3
--- PASS: TestDemo3 (2.01s)
PASS

加載(load)

加載操做在進行時只會有一個,不會有其它的讀寫操做同時進行。隊列

func TestDemo4(t *testing.T) {
    var counter int64 = 0

    for i := 0; i < 100; i++ {
        go func() {
            atomic.AddInt64(&counter, 1)
            log.Println("counter:", atomic.LoadInt64(&counter))
        }()
    }

    time.Sleep(2 * time.Second)
}

存儲(store)

存儲操做在進行時只會有一個,不會有其它的讀寫操做同時進行。

func TestDemo5(t *testing.T) {
    var counter int64 = 0

    for i := 0; i < 10; i++ {
        go func(i int) {
            atomic.StoreInt64(&counter, int64(i))
            log.Println("counter:", atomic.LoadInt64(&counter))
        }(i)
    }

    time.Sleep(2 * time.Second)
}

交換(swap)

swap 方法返回被替換以前的舊值。

func TestDemo6(t *testing.T) {
    var counter int64 = 0

    for i := 0; i < 10; i++ {
        go func(i int) {
            log.Println("counter old:", atomic.SwapInt64(&counter, int64(i)))
        }(i)
    }

    time.Sleep(2 * time.Second)
}

結果

=== RUN   TestDemo6
2020/10/11 00:43:36 counter old: 0
2020/10/11 00:43:36 counter old: 9
2020/10/11 00:43:36 counter old: 5
2020/10/11 00:43:36 counter old: 1
2020/10/11 00:43:36 counter old: 2
2020/10/11 00:43:36 counter old: 3
2020/10/11 00:43:36 counter old: 6
2020/10/11 00:43:36 counter old: 4
2020/10/11 00:43:36 counter old: 7
2020/10/11 00:43:36 counter old: 0
--- PASS: TestDemo6 (2.00s)
PASS

原子值(value)

value是一個結構體,內部值定義爲 interface{},因此它是能夠接受任何類型的值。

第一次賦值的時候,原子值的類型就確認了,後面不能賦值其它類型的值。

func TestDemo7(t *testing.T) {
    var value atomic.Value
    var counter uint64 = 1

    value.Store(counter)
    log.Println("counter:", value.Load())

    value.Store(uint64(10))
    log.Println("counter:", value.Load())

    value.Store(100) // 引起 panic
    log.Println("counter:", value.Load())

    time.Sleep(2 * time.Second)
}

結果

=== RUN   TestDemo7
2020/10/11 10:14:58 counter: 0
2020/10/11 10:14:58 counter: 10
--- FAIL: TestDemo7 (0.00s)
panic: sync/atomic: store of inconsistently typed value into Value [recovered]
    panic: sync/atomic: store of inconsistently typed value into Value
                ...
Process finished with exit code 1

擴展

無鎖編程

此處暫時先介紹一下,後面有機會出文章再一塊兒學習進步。

放棄互斥鎖,採用原子操做,常見方法有如下幾種:

針對計數器

可使用例如上面介紹的 Add 方法。

單生產、消費者

單生產者、單消費者能夠作到免鎖訪問環形緩衝區(Ring Buffer)。
好比,Linux kernel 中的 kfifo 的實現。

RCU(Read Copy Update)

新舊副本切換機制,對於舊副本能夠採用延遲釋放的作法。

CAS(Compare And Swap)

如無鎖棧,無鎖隊列等待

總結

  1. 原子操做性能是高於互斥鎖的,但帶來的複雜性也會提升,真正用好並不容易。
  2. 互斥鎖、條件變量,方法內部的實現也都用到了原子操做,特別是CAS。

文章示例代碼

相關文章
相關標籤/搜索