[TOC]
# 簡介
* 加鎖代價比較耗時,需要上下文切換
* 針對基本數據類型,可以使用原子操作保證線程安全
* 原子操作在用戶態就可以完成,因此性能比互斥鎖要高
sync/atomic包中的函數可以做的原子操作有:
* 加法(add)
* 比較并交換(compare and swap,簡稱 CAS)
* 加載(load)
* 存儲(store)
* 交換(swap)
原子操作函數需要的是被操作值的指針,而不是這個值本身
**只要原子操作函數拿到了被操作值的指針**,就可以定位到存儲該值的內存地址。只有這樣,它們才能夠通過底層的指令,準確地操作這個內存地址上的數據。
# 支持的類型
這些函數針對的數據類型并不多。但是,對這些類型中的每一個,
sync/atomic包都會有一套函數給予支持。這些數據類型有:int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer。
不過,針對unsafe.Pointer類型,該包并未提供進行原子加法操作的函數。
# 減法
atomic.AddInt32函數的第二個參數代表差量,它的類型是int32,是有符號的。如果我們想做原子減法,那么把這個差量設置為負整數就可以了。
對于atomic.AddInt64函數來說也是類似的。不過,要想用atomic.AddUint32和atomic.AddUint64函數做原子減法,就不能這么直接了,因為它們的第二個參數的類型分別是uint32和uint64,都是無符號的,不過,這也是可以做到的,就是稍微麻煩一些。
例如,如果想對uint32類型的被操作值18做原子減法,比如說差量是\-3,那么我們可以先把這個差量轉換為有符號的int32類型的值,然后再把該值的類型轉換為uint32,用表達式來描述就是uint32(int32(-3))。
不過要注意,直接這樣寫會使 Go 語言的編譯器報錯,它會告訴你:“常量\-3不在uint32類型可表示的范圍內”,換句話說,這樣做會讓表達式的結果值溢出。
不過,如果我們先把int32(-3)的結果值賦給變量delta,再把delta的值轉換為uint32類型的值,就可以繞過編譯器的檢查并得到正確的結果了。
最后,我們把這個結果作為atomic.AddUint32函數的第二個參數值,就可以達到對uint32類型的值做原子減法的目的了。
還有一種更加直接的方式。我們可以依據下面這個表達式來給定atomic.AddUint32函數的第二個參數值:
~~~
^uint32(-N-1))
~~~
其中的N代表由負整數表示的差量。也就是說,我們先要把差量的絕對值減去1,然后再把得到的這個無類型的整數常量,轉換為uint32類型的值,最后,在這個值之上做按位異或操作,就可以獲得最終的參數值了。
這么做的原理也并不復雜。簡單來說,此表達式的結果值的補碼,與使用前一種方法得到的值的補碼相同,所以這兩種方式是等價的。我們都知道,整數在計算機中是以補碼的形式存在的,所以在這里,結果值的補碼相同就意味著表達式的等價
# 比較并交換和交換操作相比有什么不同?
比較并交換操作即 CAS 操作,是有條件的交換操作,只有在條件滿足的情況下才會進行值的交換。
所謂的交換指的是,把新值賦給變量,并返回變量的舊值。
在進行 CAS 操作的時候,函數會先判斷被操作變量的當前值,是否與我們預期的舊值相等。如果相等,它就把新值賦給該變量,并返回true以表明交換操作已進行;否則就忽略交換操作,并返回false。
可以看到,CAS 操作并不是單一的操作,而是一種操作組合。這與其他的原子操作都不同。正因為如此,它的用途要更廣泛一些。例如,我們將它與for語句聯用就可以實現一種簡易的自旋鎖(spinlock)。
~~~
for {
if atomic.CompareAndSwapInt32(&num2, 10, 0) {
fmt.Println("The second number has gone to zero.")
break
}
time.Sleep(time.Millisecond * 500)
}
~~~
for語句加 CAS 操作的假設往往是:共享資源狀態的改變并不頻繁,或者,它的狀態總會變成期望的那樣。這是一種更加樂觀,或者說更加寬松的做法。
# 方法
目前只支持int類型

~~~
import (
"fmt"
"sync"
"sync/atomic"
)
var x int32
var wg sync.WaitGroup
func add() {
for i := 0; i<5000; i++ {
//x = x+1
atomic.AddInt32(&x, 1)
}
defer wg.Done()
}
func main() {
wg.Add(2)
//各加5000
go add()
go add()
wg.Wait()
//結果一定是10000
fmt.Println(x)
}
~~~
# 自旋鎖
~~~
// forAndCAS1 用于展示簡易的自旋鎖。
func forAndCAS1() {
sign := make(chan struct{}, 2)
num := int32(0)
fmt.Printf("The number: %d\n", num)
go func() { // 定時增加num的值。
defer func() {
sign <- struct{}{}
}()
for {
time.Sleep(time.Millisecond * 500)
newNum := atomic.AddInt32(&num, 2)
fmt.Printf("The number: %d\n", newNum)
if newNum == 10 {
break
}
}
}()
go func() { // 定時檢查num的值,如果等于10就將其歸零。
defer func() {
sign <- struct{}{}
}()
for {
if atomic.CompareAndSwapInt32(&num, 10, 0) {
fmt.Println("The number has gone to zero.")
break
}
time.Sleep(time.Millisecond * 500)
}
}()
<-sign
<-sign
}
~~~
# atomic.value
atomic.Value分為兩個操作,通過Store()存儲Value,通過Load()來讀取Value的值.
~~~
package main
import (
"fmt"
"sync/atomic"
)
type Value struct {
key string
Val interface{}
}
type Noaway struct {
Movies atomic.Value
Total atomic.Value
}
func NewNoaway() *Noaway {
n := new(Noaway)
n.Movies.Store(&Value{key: "movie", Val: "Wolf Warrior 2"})
n.Total.Store("123")
return n
}
func main() {
n := NewNoaway()
val := n.Movies.Load().(*Value)
total := n.Total.Load().(string)
fmt.Printf("%#v --- %#v\n", val, total)
}
~~~
## 如何用好atomic.value
第一條規則,不能用原子值存儲nil。
也就是說,我們不能把nil作為參數值傳入原子值的Store方法,否則就會引發一個 panic。
這里要注意,如果有一個接口類型的變量,它的動態值是nil,但動態類型卻不是nil,那么它的值就不等于nil。我在前面講接口的時候和你說明過這個問題。正因為如此,這樣一個變量的值是可以被存入原子值的。
第二條規則,我們向原子值存儲的第一個值,決定了它今后能且只能存儲哪一個類型的值。
例如,我第一次向一個原子值存儲了一個string類型的值,那我在后面就只能用該原子值來存儲字符串了。如果我又想用它存儲結構體,那么在調用它的Store方法的時候就會引發一個 panic。這個 panic 會告訴我,這次存儲的值的類型與之前的不一致。
你可能會想:我先存儲一個接口類型的值,然后再存儲這個接口的某個實現類型的值,這樣是不是可以呢?
很可惜,這樣是不可以的,同樣會引發一個 panic。因為原子值內部是依據被存儲值的實際類型來做判斷的。所以,即使是實現了同一個接口的不同類型,它們的值也不能被先后存儲到同一個原子值中。
遺憾的是,我們無法通過某個方法獲知一個原子值是否已經被真正使用,并且,也沒有辦法通過常規的途徑得到一個原子值可以存儲值的實際類型。這使得我們誤用原子值的可能性大大增加,尤其是在多個地方使用同一個原子值的時候。
下面,我給你幾條具體的使用建議。
1. 不要把內部使用的原子值暴露給外界。比如,聲明一個全局的原子變量并不是一個正確的做法。這個變量的訪問權限最起碼也應該是包級私有的。
2. 如果不得不讓包外,或模塊外的代碼使用你的原子值,那么可以聲明一個包級私有的原子變量,然后再通過一個或多個公開的函數,讓外界間接地使用到它。注意,這種情況下不要把原子值傳遞到外界,不論是傳遞原子值本身還是它的指針值。
3. 如果通過某個函數可以向內部的原子值存儲值的話,那么就應該在這個函數中先判斷被存儲值類型的合法性。若不合法,則應該直接返回對應的錯誤值,從而避免 panic 的發生。
4. 如果可能的話,我們可以把原子值封裝到一個數據類型中,比如一個結構體類型。這樣,我們既可以通過該類型的方法更加安全地存儲值,又可以在該類型中包含可存儲值的合法類型信息。
除了上述使用建議之外,我還要再特別強調一點:盡量不要向原子值中存儲引用類型的值。因為這很容易造成安全漏洞。請看下面的代碼:
~~~
var box6 atomic.Value
v6 := []int{1, 2, 3}
box6.Store(v6)
v6[1] = 4 // 注意,此處的操作不是并發安全的!
~~~
我把一個[]int類型的切片值v6, 存入了原子值box6。注意,切片類型屬于引用類型。所以,我在外面改動這個切片值,就等于修改了box6中存儲的那個值。這相當于繞過了原子值而進行了非并發安全的操作。那么,應該怎樣修補這個漏洞呢?可以這樣做:
~~~
store := func(v []int) {
replica := make([]int, len(v))
copy(replica, v)
box6.Store(replica)
}
store(v6)
v6[2] = 5 // 此處的操作是安全的
~~~
- 基礎
- 簡介
- 主要特征
- 變量和常量
- 編碼轉換
- 數組
- byte與rune
- big
- sort接口
- 和mysql類型對應
- 函數
- 閉包
- 工作區
- 復合類型
- 指針
- 切片
- map
- 結構體
- sync.Map
- 隨機數
- 面向對象
- 匿名組合
- 方法
- 接口
- 權限
- 類型查詢
- 異常處理
- error
- panic
- recover
- 自定義錯誤
- 字符串處理
- 正則表達式
- json
- 文件操作
- os
- 文件讀寫
- 目錄
- bufio
- ioutil
- gob
- 棧幀的內存布局
- shell
- 時間處理
- time詳情
- time使用
- new和make的區別
- container
- list
- heap
- ring
- 測試
- 單元測試
- Mock依賴
- delve
- 命令
- TestMain
- path和filepath包
- log日志
- 反射
- 詳解
- plugin包
- 信號
- goto
- 協程
- 簡介
- 創建
- 協程退出
- runtime
- channel
- select
- 死鎖
- 互斥鎖
- 讀寫鎖
- 條件變量
- 嵌套
- 計算單個協程占用內存
- 執行規則
- 原子操作
- WaitGroup
- 定時器
- 對象池
- sync.once
- 網絡編程
- 分層模型
- socket
- tcp
- udp
- 服務端
- 客戶端
- 并發服務器
- Http
- 簡介
- http服務器
- http客戶端
- 爬蟲
- 平滑重啟
- context
- httptest
- 優雅中止
- web服務平滑重啟
- beego
- 安裝
- 路由器
- orm
- 單表增刪改查
- 多級表
- orm使用
- 高級查詢
- 關系查詢
- SQL查詢
- 元數據二次定義
- 控制器
- 參數解析
- 過濾器
- 數據輸出
- 表單數據驗證
- 錯誤處理
- 日志
- 模塊
- cache
- task
- 調試模塊
- config
- 部署
- 一些包
- gjson
- goredis
- collection
- sjson
- redigo
- aliyunoss
- 密碼
- 對稱加密
- 非對稱加密
- 單向散列函數
- 消息認證
- 數字簽名
- mysql優化
- 常見錯誤
- go run的錯誤
- 新手常見錯誤
- 中級錯誤
- 高級錯誤
- 常用工具
- 協程-泄露
- go env
- gometalinter代碼檢查
- go build
- go clean
- go test
- 包管理器
- go mod
- gopm
- go fmt
- pprof
- 提高編譯
- go get
- 代理
- 其他的知識
- go內存對齊
- 細節總結
- nginx路由匹配
- 一些博客
- redis為什么快
- cpu高速緩存
- 常用命令
- Go 永久阻塞的方法
- 常用技巧
- 密碼加密解密
- for 循環迭代變量
- 備注
- 垃圾回收
- 協程和纖程
- tar-gz
- 紅包算法
- 解決golang.org/x 下載失敗
- 逃逸分析
- docker
- 鏡像
- 容器
- 數據卷
- 網絡管理
- 網絡模式
- dockerfile
- docker-composer
- 微服務
- protoBuf
- GRPC
- tls
- consul
- micro
- crontab
- shell調用
- gorhill/cronexpr
- raft
- go操作etcd
- mongodb