1、首先先把配置文件從jar中抽離 示例代碼: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <confi ...
atomic
和sema
是實現go
中鎖的基礎,簡單看下他們的實現原理。
atomic
atomic
常用來作為保證原子性的操作。
當多個協程,同時一個數據進行操作時候,如果不加鎖,最終的很難得到想要的結果。
var p int64 = 0
func add() {
p = p + 1
}
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(time.Second * 5)
fmt.Println(p) //982
}
這種情況下,最終列印的 都不會是1000,每次不固定。
改成atomic 能解決
var p int64 = 0
func add() {
atomic.AddInt64(&p, 1)
}
func main() {
for i := 0; i < 1000; i++ {
go add()
}
time.Sleep(time.Second * 5)
fmt.Println(p)
}
atomic 為什麼能做到?
TEXT sync∕atomic·AddInt64(SB), NOSPLIT, $0-24
GO_ARGS
MOVD $__tsan_go_atomic64_fetch_add(SB), R9
BL racecallatomic<>(SB)
MOVD add+8(FP), R0 // convert fetch_add to add_fetch
MOVD ret+16(FP), R1
ADD R0, R1, R0
MOVD R0, ret+16(FP)
RET
老的版本中是能見到,lock 這種操作系統級別的鎖,新版的go已經改寫了這塊邏輯,但是能猜想到效果肯定一樣。 如果有理清楚的,評論區可以交流下。
小結:
原子操作是一種硬體層面加鎖的機制
保證操作一個變數的時候,其他協程、線程無法訪問
sema
幾乎在go的每個鎖的定義都能看到sema
的身影,理解了sema
再看 互斥鎖、讀寫鎖就會很好理解。
信號量鎖/信號鎖
核心是一個uint32值,含義是同時可併發的數量
每一個sema 鎖都對應一個SemaRoot結構體
SemaRoot中有一個平衡二叉樹用於協程排隊
例如:
type Mutex struct {
state int32
sema uint32
}
sema的uint32中的 每一個數,背後都對應一個 semaRoot的結構體
type semaRoot struct {
lock mutex
treap *sudog // root of balanced tree of unique waiters.
nwait atomic.Uint32 // Number of waiters. Read w/o the lock.
}
type sudog struct {
g *g // 包含了 協程 g
next *sudog // 下一個
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
}
結構如下:
這裡可以先講下,當這個 sema uint32
值,初始化時候,大於0 比如 賦值5,就代表著,併發時候,有5個協程可以獲取鎖。其他協程需要等待前面5個釋放了,才能進入。
sema 大於0
// 獲取sema鎖。大於0的情況
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
gp := getg()
if gp != gp.m.curg {
throw("semacquire not on the G stack")
}
// Easy case. // 容易的情況
if cansemacquire(addr) {
return
}
// 方法很長,先看簡單的部分。
}
func cansemacquire(addr *uint32) bool {
for {
v := atomic.Load(addr) // 根據sema的地址,獲取int32的值
if v == 0 { // 如果未0了,就獲取失敗了
return false
}
// 大於0 ,則把 sema的值減去1
if atomic.Cas(addr, v, v-1) { // cas 就是 CompareAndSwapInt 的底層實現
return true
}
}
}
到此,對sema為什麼只是定義為一個 uint32的值有了大致理解,就是一個控制能有多少個協程同時獲取鎖的值。
看下釋放:
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semtable.rootFor(addr)
atomic.Xadd(addr, 1) // 給sema的值 加上1
// Easy case: no waiters?
// This check must happen after the xadd, to avoid a missed wakeup
// (see loop in semacquire).
if root.nwait.Load() == 0 { // 如果沒有 nwait在等待,就直接結束。
return
}
}
nwait 就是等待協程的個數。
小結, 當sema的值大於0 :
獲取鎖:uint32減1 ,獲取成功
釋放鎖:uint32加1,釋放成功
sema值等於0
再看 semacquire1
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) {
/ / Harder case:
// increment waiter count
s := acquireSudog()
root := semtable.rootFor(addr) // 根據sema的地址,獲取了包含 sudog的隊列
for {
root.queue(addr, s, lifo) // 將新的協程放入這個等待隊列中
goparkunlock(&root.lock, reason, traceBlockSync, 4+skipframes)
// 主動調用協程的gopark,讓它休眠,gopark的說明看 go GMP中有講
}
releaseSudog(s)
}
//再看釋放
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semtable.rootFor(addr)
atomic.Xadd(addr, 1)
if root.nwait.Load() == 0 {
return
}
// 如果等待隊列不是0 ,就需要釋放一個
// Harder case: search for a waiter and wake it.
lockWithRank(&root.lock, lockRankRoot)
s, t0 := root.dequeue(addr) // 從全局的隊列中,取出一個
if s != nil {
root.nwait.Add(-1) // 把等待的數量減一
}
unlock(&root.lock) //操作全局隊列都需要加鎖
}
小結,當sema的值等於0時候:
獲取鎖:協程休眠,進入堆樹等待
釋放鎖:從堆樹中取出一個協程,喚醒
sema 鎖退化成一個專用休眠隊列
有沒有可能sema的值,小於0 ?
看看sema的定義 `uint32` 所以 不可能。
總結下:
atomic原子操作是一種硬體層面的加鎖機制。
sema 背後是一整套鎖的管理和等待的機制,開發者在使用時候,感知不到。
sema的值就是能同時獲取鎖協程的個數。sema的地址作為了休眠等待隊列(平衡樹)的key。