在大多数的语言里,赋值操作是原子性(不可拆分)的,但是Golang中并不是,Golang里很多类型的赋值操作是分多走的,而多步走就会导致在不同协程里操作同一个变量会出现异常,那么哪些类型的变量是并发赋值安全的,哪些是不安全的呢?

先说答案

Golang 中数据类型可以分类两大类:基本数据类型和复合数据类型

基本数据类型有:字节型(安全),布尔型(安全)、整型(安全)、浮点型(安全)、字符型(安全)、复数型(不安全)、字符串(不安全)。

复合数据类又可细分为如下三类:

  • 非引用类型:数组(不安全)、结构体(不安全);

  • 引用类型:指针(安全)、切片(不安全)、字典(不安全)、通道(不安全)、函数(安全);

  • 接口(不安全)。

并发不安全的原因

只有一个原因就是多步操作导致的,下面拿最多人搞错的字符串举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package mian

import (
	"sync"
	"testing"
)

//并发赋值字符串
func TestString(t *testing.T) {
	var s string

	n := 1000000
	var i int
	for ; i < n; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "ab"
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "abc"
		}()
		wg.Wait()

		// 赋值异常判断
		if s != "ab" && s != "abc" {
			t.Logf("concurrent assignment error, i=%v s=%v", i, s)
			break
		}
	}

	if i == n {
		t.Logf("no error")
	}
}

上面的代码很容易能看懂:一个协程不断地给s赋值ab,一个协程给s赋值abc,如果s既不等于ab也不等于abc就会跳出循环。

不了解Golang的人可能会想,不可能会出现既不是ab也不是abc的情况,然而事实是很容易就进入这个情况。

要解释这个就得去看源码包src/runtime/string.go里string的底层结构:

1
2
3
4
type stringStruct struct {
    str unsafe.Pointer
    len int
}

str 为字符串的首地址

len 为字符串的长度(单位字节)

因为string 底层结构是个 struct,还有两个字段,并发操作下经常会出现给str赋值了,另一个给len赋值了,就会出现str=ab,len=3,这样取出来的就不是ab了,也不是abc(当然有一定几率是abc,得看str地址下的第三个是不是刚好是c)

怎么并发安全赋值

看了上面的例子,很多小伙伴可能会想:天呐,还好看了这篇文章,不然出了线上安全事故可就惨了,这bug谁能找到啊!

那么如果出现并发操作同一个变量,如何保证并发安全呢?

加锁

加锁的例子有很多,最常见的就是sync.RWMutex,当然不同类型的变量可以使用其他第三方(或官方)的包,下面列举几个,就不一一列代码了:

  • sync.RWMutex
  • sync.Map
  • concurrent-map
  • atomic.Value

使用channel

除了加锁的方式,就剩下channel了,这里就要介绍下Golang的并发座右铭,在《Effective Go》的channel介绍中写到:

Share memory by communicating, don’t communicate by sharing memory.

通过通信共享内存,而不是通过共享内存而通信。

上面的话的意思是面对并发问题,你首先想到的应该是channel,因为channel是线程安全的并且不会有数据冲突,比锁好用,但要使用channel还是锁需要具体分析,并不是说channel可以替代锁,因为有些场景channel的性能并不比锁来得高效(事实上大多数情况下锁更高效,感兴趣的同学可以自己试一下,希望有一天Golang会把channel优化到我把这个括号去掉)

延伸阅读

Go 语言标准库中 atomic.Value 的前世今生

Go sync.Map 实现》–此文详细分析了sync.Map的实现原理

Mutex Or Channel》–mutex和channel如何选择

Atomic、Mutex、Channel比较》–这三者的性能比较