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 mainimport (    "fmt"    "sort"    "time")var s []intfunc 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 : 01 : 02 : 03 : 04 : 0...80 : 081 : 082 : 083 : 084 : 085 : 186 : 287 : 388 : 689 : 890 : 1291 : 1392 : 1493 : 1994 : 2895 : 3096 : 3197 : 3298 : 3399 : 34100 : 35101 : 36102 : 44103 : 45104 : 46105 : 47106 : 48107 : 49108 : 50109 : 51110 : 52111 : 53112 : 54113 : 55114 : 56115 : 57...8328 : 99908329 : 99918330 : 99928331 : 99938332 : 99958333 : 99968334 : 99948335 : 99978336 : 99988337 : 9999

最初没有到 9999 : 9999,但也没出任何提醒


package mainimport (    "fmt"    "time")const N = 500func 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 writegoroutine 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.gotype 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多平台公布