共计 4392 个字符,预计需要花费 11 分钟才能阅读完成。
Go 一共有 27 种细分数据类型 (可参考 利用反射, 探索 Go 语言中的数据类型)
除 channel 外 (构造体中有 mutex,保障其余字段的并发平安),个别状况下,byte,bool,int,float,point,func 是并发平安的
(这些数据类型的位宽不会超过 64 位,所以在 64 位的指令集架构中能够由一条机器指令实现,不存在被细分为更小的操作单位,故而这些类型的并发赋值是平安的;但也和操作系统的位数无关,如 int64 在 32 位操作系统中,高 32 位和低 32 位是离开赋值的,此时是非并发平安的)
而 string,slice,map 这三种最罕用的数据结构是并发不平安的
(interface,complex,struct,数组,往往也是并发不平安的)
参考:
golang 之 string 类型变量操作的原子性
golang 之 slice 并发拜访
golang 之 map 并发拜访
package main
import (
"fmt"
"sort"
"time"
)
var s []int
func appendValue(i int) {s = append(s, i)
}
func main() {
for i := 0; i < 10000; i++ {go appendValue(i)
}
sort.Ints(s) // 给切片排序, 先排完序再打印,
for i, v := range s {fmt.Println(i, ":", v)
}
time.Sleep(5e9)
}
输入为:
0 : 0
1 : 0
2 : 0
3 : 0
4 : 0
...
80 : 0
81 : 0
82 : 0
83 : 0
84 : 0
85 : 1
86 : 2
87 : 3
88 : 6
89 : 8
90 : 12
91 : 13
92 : 14
93 : 19
94 : 28
95 : 30
96 : 31
97 : 32
98 : 33
99 : 34
100 : 35
101 : 36
102 : 44
103 : 45
104 : 46
105 : 47
106 : 48
107 : 49
108 : 50
109 : 51
110 : 52
111 : 53
112 : 54
113 : 55
114 : 56
115 : 57
...
8328 : 9990
8329 : 9991
8330 : 9992
8331 : 9993
8332 : 9995
8333 : 9996
8334 : 9994
8335 : 9997
8336 : 9998
8337 : 9999
最初没有到 9999 : 9999,但也没出任何提醒
package main
import (
"fmt"
"time"
)
const N = 500
func main() {m := make(map[int]int)
go func() {
for i := 0; i < N; i++ {m[i] = i*10 + 6
}
}()
go func() {
for i := 0; i < N; i++ {fmt.Println(i, m[i])
}
}()
time.Sleep(1e9 * 5)
}
第一个协程对 map 写, 第二个对 map 读
N 较大时, 该程序将报错:
fatal error: concurrent map read and map write
goroutine 18 [running]:
runtime.throw(0x10cb62d, 0x21)
...
而且这种报错,无奈通过 recover 捕捉
看代码可知,除了并发读写 / 写写 map 这种 case,还有另外几种状况,同样无奈通过 recover 复原,须要特地留神:
- 堆栈内存耗尽 (如递归): fatal error: stack overflow
- 将 nil 函数作为 goroutine 启动 fatal error: go of nil func value
- goroutines 死锁 fatal error: all goroutines are asleep – deadlock!
- 线程超过设置的最大限度 fatal error: thread exhaustion
- 超出可用内存 fatal error: runtime: out of memory
等
那 map 和 slice 同样作为非并发平安的数据结构,为什么 map 被设计成在有并发抵触时抛出一个无奈复原的致命谬误,而 slice 却没有任何提醒?是否有任何设计上的考量?
在 golang-nuts 上提出了这个问题
沉闷于社区手不释卷的 Ian Lancer 大佬给出了如上回复
即检测 map 的并发问题非常容易 * 低成本,而检测 slice 的并发问题很艰难 & 代价昂扬
sliceheader:
// runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
mapheader
// runtime/map.go
// flags
iterator = 1 // there may be an iterator using buckets
oldIterator = 2 // there may be an iterator using oldbuckets
hashWriting = 4 // a goroutine is writing to the map
sameSizeGrow = 8 // the current map growth is to a new map of the same size
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
相比于 slice 的 ” 简洁 ”(其实扩容也挺简单的),map 要简约庞杂得多
其中 hmap 中的 flags 字段,用于存储哈希表的各种标记位信息。该字段是一个无符号整数类型(uint8)。
flags 字段的位示意在哈希表中具备不同的含意。上面是 flags 字段的各个位示意的标记位含意:
-
低 2 位(least significant 2 bits):示意哈希表的状态。具体取值如下:
- 00:哈希表为空。
- 01:哈希表正在被应用。
- 10:哈希表正在被迭代(遍历)。
- 11:哈希表正在被扩容。
- 第 3 位(bit 2):示意哈希表是否应用指针作为键(key)的布尔标记位。取值为 1 示意应用指针作为键,取值为 0 示意应用非指针类型作为键。
- 第 4 位(bit 3):示意哈希表的键(key)是否为字符串类型的布尔标记位。取值为 1 示意键为字符串类型,取值为 0 示意键为非字符串类型。
- 第 5 位(bit 4):示意哈希表是否为幕后构造的布尔标记位。取值为 1 示意该哈希表为幕后构造(backing store),即哈希表是另一个哈希表的底层数据结构。
- 第 6 位(bit 5):示意哈希表是否禁用完整性检查的布尔标记位。取值为 1 示意禁用完整性检查,取值为 0 示意启用完整性检查。
- 第 7 位(bit 6):保留位,未应用。
这些标记位用于在哈希表的操作和状态之间进行标识和传递信息。通过 flags 字段,能够理解哈希表的状态、键的类型、底层构造等信息,从而在哈希表的实现中进行相应的逻辑解决和优化。
本文由 mdnice 多平台公布