這是一個Stackoverflow上的問題但其實我去年就問過這個問題,但是被社區刪除了,因為他們覺得引發了數據競態報告就理應加鎖,不需要討論。但是在一些場景中,性能影響是需要考慮的,實際工作中也不可避免地用到各種奇淫技巧,所以這是值得討論的。現在我找到了答案。
場景
這個問題其是隻適用於少數情況,比如對於一個一寫多讀的Map,你可以理解為它是“只讀”的Map。如果對其進行替換,不管是用鎖還是不用鎖,期望的行為就是替換前是舊Map,替換後是新Map,替換的瞬間,新舊Map同時存在,這是不可避免也是理所當然的。所以唯一需要討論的就是,不使用鎖是否會產生恐慌或是讀取到不存在的錯誤數據?
太長不看:
從底層角度看,在絕大多數處理器架構上以及已知的Go版本中,對於map的覆蓋寫入是線程安全的。這很像C/C++中的實現定義行為(IB,Implementation-defined Behaviour)。但GO官方並不鼓勵這種行為,他們覺得,對於數據競態,應該採用宏觀的串行措施保證程序安全,所以對任何宏觀上併發讀寫的變量都會產生數據競態警告。
詳細:
首先,很多人覺得map是hmap結構體,但是嚴格來説並不是這樣,更準確來説map只是一個指向hmap的指針。因此,map的大小不會超過一個機器字長,unsafe.Sizeof(map1)在32位系統上顯示4字節,在64位系統上顯示8字節。
其次,你提到了機器字長,所以你應該知道,對於不超過機器字長的類型賦值,在絕大多數CPU中,是可以只通過一條指令完成的。那對map進行覆蓋賦值用了多少條指令呢?
我們寫一個這樣的程序,左邊是行號。為了更加嚴謹,我們對m1賦值兩次,原因後面會説。
1 package main
2
3 func main() {
4 m1 := make(map[int]int, 20)
5 m2 := make(map[int]int, 20)
6 m1 = m2
7 m1 = m2
8 _ = m1
9 }
將代碼生成Go彙編。
go build -gcflags="-S -N -l" main.go 1> goasm.txt 2>&1
我們可以看到,Go彙編代碼顯示第6行只用到了一條Go彙編語句。但是第7行用了兩條,但是我們發現都只是讀寫寄存器而已,將數據從內存讀到寄存器,從寄存器寫入內存,這個過程不存在中間態,因為一個核心的寄存器不存在被其他核心覆蓋的可能。所以核心操作只有一條,就是將寄存器賦值給內存MOV REG MEM。
0x0070 00112 (main.go:6) MOVQ AX, main.m1+32(SP)
0x0075 00117 (main.go:7) MOVQ main.m2+24(SP), DX
0x007a 00122 (main.go:7) MOVQ DX, main.m1+32(SP)
由於Go彙編語句只是偽彙編,為的是在不同硬件平台上都能編譯。所以我我們直接反彙編。
objdump -S -d main > objdump.txt
在amd64 linux機器上,反彙編的結果如下。核心操作是將寄存器的值寫入內存。
m1 = m2
4576f0: 48 89 44 24 20 mov %rax,0x20(%rsp)
m1 = m2
4576f5: 48 8b 54 24 18 mov 0x18(%rsp),%rdx
4576fa: 48 89 54 24 20 mov %rdx,0x20(%rsp)
所以,問題2,MOV操作是原子的嗎?
是的,在現今絕大多數的架構上,對於內存對齊的不長於機器字長的MOV操作,都是原子的。
參考Go的內存模型,Go內存對齊策略,英特爾64架構開發手冊。
Otherwise, a read r of a memory location x that is not larger than a machine word must observe some write w such that r does not happen before w and there is no write w' such that w happens before w' and w' happens before r. That is, each read must observe a value written by a preceding or concurrent write.
這段話很晦澀,但總的意思是每一次不超過字長的讀寫都是完整的、原子的,不存在讀/寫了一半的情況。
The following minimal alignment properties are guaranteed:
1. For a variable x of any type: unsafe.Alignof(x) is at least 1. 2. For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1. 3. For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
Go保證變量在內存以某種方式對齊。
8.1.1 Guaranteed Atomic Operations
The Intel486 processor (and newer processors since) guarantees that the following basic memory operations will
always be carried out atomically:
• Reading or writing a byte
• Reading or writing a word aligned on a 16-bit boundary
• Reading or writing a doubleword aligned on a 32-bit boundary
The Pentium processor (and newer processors since) guarantees that the following additional memory operations
will always be carried out atomically:
• Reading or writing a quadword aligned on a 64-bit boundary
• 16-bit accesses to uncached memory locations that fit within a 32-bit data bus
The P6 family processors (and newer processors since) guarantee that the following additional memory operation
will always be carried out atomically:
• Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line
英特爾開發手冊保證對於目前絕大多數英特爾處理器,對不超過64位的寄存器讀寫操作保證原子。
所以答案是,可以,在絕大多數處理器架構上以及已知的Go版本中,對於map的覆蓋寫入是線程安全的。儘管Go官方並不鼓勵這種做法,並警告到
編寫無數據競爭的Go程序的程序員,可以依賴這些程序的串行執行,就像其他現代編程語言一樣。
對於包含數據競爭的程序來説,無論是程序員還是編譯器,都應該記住一個建議:不要過於聰明。
- [1]The Go Memory Model
- [2]Size and alignment guarantees
- [3]Intel® 64 and IA-32 Architectures Developer's Manual: Vol. 3A