1、并发与并行

并发:同一时间段内执行多个人任务。

并行:同一时刻执行多个任务。

go语言中并发通过goroutine实现。goroutine雷系线程,属于用户态的线程,我们可以根据需要创建成千上万的goroutine并发工作。goroutine是由Go语言的运行时runtime调度完,而先线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine进行通信。goroutinechannel是Go语言秉承的CSP(Communicating Sequential Process)并发模式的实现基础。

2、goroutine

在java/c++中我们要实现并发编程的时候,我们为您通常需要维护一个线程池,并且需要自己包装一个有一个的任务,同时需要自己去调度线程执行任务并维护上线纹切换。

Go语言中的goroutine就是这种机制,goroutine的概念类似线程,但goroutine是由Go语言运行时runtime调度和管理的。Go程序会智能的将goroutine中的任务合理的分配给每个CPU。Go语言之所以称为现代化的编程语言,就是因为它在语言层面上已经实现了调度和上下文切换。

2.1、使用goroutine

只需要在调度的函数前加上go关键字,就可以为函数创建一个goroutine

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

2.2、启动单个goroutine

启动goroutine的方式非常简单,只需要在调用的函数/匿名函数前面加一个go关键字。

  • 默认串行执行
1
2
3
4
5
6
7
8
9
10
//该示例没有加`go`串行执行
//先打印Hello Goroutine!,后打印main goroutine done
func main() {
hello()
fmt.Println("main goroutine done")
}

func hello() {
fmt.Println("Hello Goroutine!")
}
  • 使用go关键字并行执行
1
2
3
4
5
6
7
8
9
10
11
//该示例加`go`并行行执行
//打印main goroutine done
//在新创建的goroutine还没启动完成,主goroutine(main)已经执行完毕,其创建的子goroutine也会跟随主goroutine一起结束
func main() {
go hello()
fmt.Println("main goroutine done")
}

func hello() {
fmt.Println("Hello Goroutine!")
}

启动多个goroutine

可以启动多个goroutine执行一个函数。使用sync.WaitGroup来实现goroutine同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var wg sync.WaitGroup

//使用sync.WaitGroup同步goroutine
func main() {
for i := 0; i < 10; i++ {
//创建一个goroutine就登记 +1
wg.Add(1)
go hello(i)
}
//等待所有登记的goroutine都结束
wg.Wait()
fmt.Println("main goroutine done")
}

func hello(i int) {
//goroutine结束就登记减1
defer wg.Done()
fmt.Println(i, " : Hello Goroutine!")
}

每次打印的顺序都不一样,是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

goroutine与线程

可增长的栈

OS线程(操作系统线程)一都有固定的栈内存(通常2MB),一个goroutine的栈在其生命周期开始的时候只有很小的栈(典型情况下2Kb),goroutine的栈是不固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1Gb,极少情况下会用到那么大。在Go语言中一次创建十万个左右的goroutine也是可以的。

goroutine调度

GMP是Go语言运行时runtime层面实现的,是Go语言自己实现的一套调度系统。区别于系统调度OS线程。

  • G:就是goroutine,里面除了存放本goroutine的信息外,还有与所在P的绑定信息。

  • P:管理者一组goroutine队列,P里面储存着当前goroutine运行的上下文环境(函数指针,堆栈地址以及地址边界),P会对自己管理的goroutine队列做一些调度(比如把CPU占用时间长的goroutine暂停,运行后续的goroutine等),当自己队列中消费完就去全局队列里取,如果全局队列里也消费完就会去其他P的队列里抢任务。

  • M(machine):是Go运行时(runtime)对操作系统内核线程的虚拟,M与内核的关系一般是一一映射的关系,一个goroutine一般是要放到M上执行的。

P与M一般也是一一对应的。他们的关系是:P管理着一组G挂在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载新建的M上运行。当旧的G阻塞完成或者认为其已经死掉时回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。在并发量大的时候会增加一些M和P,但不会太多,频繁切换的话会得不偿失。

单从线程调度讲,Go语言相比其它语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时runtime自己的调度器调度的,这个调度器使用一个称为m:n的调度技术(复用/调度m个goroutine到n个OS线程)。其一大特点是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着的一大块内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。另一方面充分利用了多核的硬件资源,近似把若干goroutine均分到物理线程上,再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定使用多少个OS线程来同时执行Go代码。(Go1.5版本之后)默认值是机器上CPU的核心数。

Go语言通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的的CPU逻辑核心数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var wg sync.WaitGroup

//使用runtime.GOMAXPROCES()设置并发时使用的CPU核心数
func main() {
wg.Add(2)
runtime.GOMAXPROCS(1)
go a()
b()
wg.Wait()
}

func a() {
defer wg.Done()
for i := 0; i <10; i++ {
fmt.Println("A : ", i)
}
}

func b(){
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("B : ", i)
}
}

两个任务只有一个逻辑核心,此时是做完一个任务在做另外一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var wg sync.WaitGroup

//使用runtime.GOMAXPROCES()设置并发时使用的CPU核心数
func main() {
wg.Add(2)
runtime.GOMAXPROCS(2)
go a()
b()
wg.Wait()
}

func a() {
defer wg.Done()
for i := 0; i <10; i++ {
fmt.Println("A : ", i)
}
}

func b(){
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("B : ", i)
}
}

将逻辑核心数设置为2,此时两个任务并行执行。

Go语言中的操作系统线程和goroutine的关系:

  • 1、一个操作系统线程对应用户态多个goroutine。
  • 2、go程序可以同时使用多个操作系统线程。
  • goroutine和OS线程是多对多的关系,即m:n。

channel

单纯的将函数并发是没有意义的。函数与函数之间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但其共享内存在不同的goroutine中容易出现竞态问题。为保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法会造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Procsses),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送值到另外一个goroutine的通信机制。

Go语言中的channel是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序一致。每个通道都是一个具体类型的管道,也就是申明channel时需要为其指定元素类型。

channel类型

channel是一种类型,引用类型。

1
var 变量 chan 元素类型

Example:

1
2
3
var ch1 chan int //声明一个传递整型的通道
var ch2 chan bool //声明一个传递布尔类型的通道
var ch3 chan []int //声明一个传递int切片的通道

注意:声明的通道必须使用make函数初始化之后才能使用

make(chan 元素类型, 缓冲区大小)

1
2
var ch1 chan int
ch1 = make(chan int, 8)

channel操作

通道有发送(send),接受(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

定义一个通道:

1
ch := make(chan int)

发送:

1
ch <- 10 //把10发送到通道中

接收:

1
2
x := <- ch //从ch中接收值并赋值给变量x
<- ch //从ch中接收值,忽略结果

关闭:

1
close(ch)

注意:

  • 只有在通知接收方goroutine所有数据发送完毕的时候才需要关闭通道。
  • 通道是可以被垃圾回收机制收回的,它和文件关闭是不一样的,在操作结束后关闭文件是必须做的,但关闭通道不是必须的。

通道的特点:

  • 对一个关闭的通道再发送值就会导致panic。
  • 对一个关闭的通道进行接收会一直获取直到通道为空。
  • 对一个关闭并且没有值的通道执行接收操作会得到对应的类型的零值。
  • 关闭一个已经关闭的通道会导致panic。

无缓冲区通道

无缓冲区的通道又称为无阻塞通道。

1
2
3
4
5
6
//无缓冲区的通道只有在有人接收值的时候才能发送值,不然会造成死锁
func main() {
var ch = make(chan int) //无缓冲区通道
ch <- 10 //fatal error: all goroutines are asleep - deadlock!
fmt.Println("发送成功")
}

无缓冲区的通道只有在有人接收值的时候才能发送值。

1
2
3
4
5
6
7
8
9
10
11
12
//启用一个goroutine接收值
func main() {
var ch = make(chan int) //无缓冲区通道
go recv(ch)
ch <- 10
fmt.Println("发送成功")
}

func recv(ch chan int){
x := <- ch
fmt.Println("接收成功:", x)
}

无缓冲区通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才会发送成功,两个goroutine将继续执行。如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲区的通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲区通道

在make函数初始化通道时为其指定通道的容量。

1
2
3
4
5
6
7
8
9
10
func main() {
var ch = make(chan int, 10) //带缓冲区通道
ch <- 10
ch <- 20
ch <- 30
fmt.Println("发送成功")
fmt.Println("通道内的数量为:", len(ch)) //通道内的数量为: 3
fmt.Println("通道的容量为:", cap(ch)) //通道的容量为: 10
}
}

只要通道的容量大于零,那么该通道就是有缓冲区通道,通道的容量表示通道中能存放元素的数量。

可以使用len()函数获取通道内元素的数量,使用cap()函数可以获取到通道的容量。

for range从通道中循环获取值

向通道中发送完数据时,可以使用close()函数来关闭通道。

当通道关闭时,载往该通道发送值会引发panic,从该通道取值操作会先取完通道中的值,然后取到的之一直都是对应类型的零值。

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
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
//开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
//开启goroutine从ch1中接收值,将该值平方后发送到ch2中
go func() {
for {
x, ok := <- ch1 //通道关闭后再取值
if !ok {
break
}
ch2 <- x * x
}
close(ch2)
}()
//在主goroutine中从ch2中接收值打印
for i := range ch2{
fmt.Println(i)
}
}

for range遍历通道时,当通道被关闭的时候就会退出for range

单向通道

有的时候会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其限制,比如限制通道在函数中只能发送或者接收。

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
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}

func printer(in <-chan int) {
for i := range in{
fmt.Println(i)
}
}

func squarer(out chan<- int, in <-chan int) {
for i := range in{
out <- i * i
}
close(out)
}

func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
  • chan <- int只是一个写单向通道(只能对其写入int类型),只可以对其执行发送操作但是不能对其执行接收操作。
  • <- chan int只是一个单向读通道(只能从其读取int类型值),可以对其执行接收操作但不能执行发送操作。

在函数传参时及任何赋值操作中可以将双向通道转换为单向通道,但不能从单向通道转换为双向通道。

通道总结

channel nil 非空 满了 没满
接收 阻塞 接收值 阻塞 接收值 接收值
发送 阻塞 发送值 发送值 阻塞 发送值
关闭 panic 关闭成功,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 关闭成功,读完数据后返回零值

关闭已经关闭的channel,会引发panic

select多路复用

select的使用类似于switch语句,它有一系列的case分支和一个默认分支。每个分支对应一个通道的通信(接收或者发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case

分支对应的语句。

1
2
3
4
5
6
7
8
9
10
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}

使用select语句能提高代码的可读性:

  • 可以处理一个或者多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个。
  • 对于没有caseselect{}会一直等待,可用于阻塞main函数。

作业

//使用goroutine和channel实现一个计算int64随机数各位数和的程序。
//开启一个goroutine循环生成int64类型的随机数,发送到jobChan
//开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
//主goroutine从resultChan取出结果并打印到终端输出
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
func main() {
jobChan := make(chan int64)
resultChan := make(chan int64)
//1.开启一个goroutine循环生成int64类型的随机数,发送到jobChan
go generateNum(jobChan)
//2.开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
for i := 0; i < 24; i++ {
go sumNum(jobChan, resultChan)
}
for res := range resultChan {
fmt.Println(res)
}
}

func sumNum(jobChan <-chan int64, resultChan chan<- int64) {
//循环从job取值并计算各位数之和
for {
v := <-jobChan
resultChan <- sum(v)
}
}

//循环生成int64随机数
func generateNum(jobChan chan<- int64) {
for {
rand.Seed(time.Now().UnixNano())
i := rand.Int63()
jobChan <- i
//time.Sleep(time.Millisecond * 500)
}
}

//求一个整数各位数之和
func sum(i int64) int64 {
if i < 10 {
return i
}
return (i % 10) + sum(i/10)
}

并发安全和锁

有时候在Go代码中可能存在多个goroutine同时操作一个资源(临界值),这种情况会发生竞态问题(数据竞态)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x = 0
var wg sync.WaitGroup

func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

func add(){
defer wg.Done()
for i := 0; i < 5000; i++ {
x = x + 1
}
}

输出值每次都不一致

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,他能够保证同时只有一个goroutine可以访问共享资源。

1
2
//互斥锁
var lock sync.Mutex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var x = 0
var wg sync.WaitGroup
var lock sync.Mutex

func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}

func add(){
defer wg.Done()
for i := 0; i < 5000; i++ {
//共用区域加锁
lock.Lock()
x = x + 1
//使用完毕解锁
lock.Unlock()
}
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要去加锁的,这种场景下使用读写锁是更好的一种选择。

1
2
//读写互斥锁
var rwLock sync.RWMutex

读写锁分为两种:

当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

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
var(
x = 0
wg sync.WaitGroup
//读写互斥锁
rwLock sync.RWMutex
)

func read(){
defer wg.Done()
rwLock.RLock()
fmt.Println(x)
time.Sleep(time.Millisecond)
rwLock.RUnlock()
}

func write(){
defer wg.Done()
rwLock.Lock()
x = x + 1
time.Sleep(time.Millisecond * 5)
rwLock.Unlock()
}

func main() {
start := time.Now()
for i := 0; i < 1000; i++ {
go write()
wg.Add(1)
}
for i := 0; i < 10000; i++ {
go read()
wg.Add(1)
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}

sync.WaitGroup

Go语言可以使用sync.WaitGroup来实现并发任务同步的。

方法名 功能
func (wg *WaitGroup) Add(delta int) 计数器+delta
func (wg *WaitGroup) Done() 计数器减一
func (wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护者一个计数器,计数器的只可以增加减少。例如当我们启动了N个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1.通过调用Wait()方法来等待并发任务执行完,当计数器值为0时,表示所有任务已完成。

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
func f1() {
for i := 0; i < 10; i++ {
rand.Seed(time.Now().UnixNano())
i1 := rand.Int()
i2 := rand.Intn(10)
fmt.Println(i1, i2)
}
}

func f2(i int) {
defer wg.Done()
time.Sleep(time.Second * time.Duration(rand.Intn(3)))
fmt.Println(i)
}

var wg sync.WaitGroup

func main() {
f1()
for i := 0; i < 10; i++ {
wg.Add(1)
go f2(i)
}
//wg的计数器变为0,程序执行结束
wg.Wait()
}

注意sync.WaitGroup是一个结构体,传递的时候要传指针

sync.Once

这是一个进阶知识点。

在很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件,只关闭一次通道等。

1
var once sync.Once

sync.Once只有一个Do方法

1
func (o *Once) Do(f func())

备注:如果要执行的函数f需要传递参数就需要搭配闭包来使用

加载配置文件示例

延迟开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序启动时的消耗,而且有可能实际过程中这个变量没用上,该操作就是不必要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var icons map[string]image.Image

func loadIcons(){
icons = map[string]image.Image{
"left" : loadIcon("left.png"),
"up" : loadIcon("up.png"),
"right" : loadIcon("right.png"),
"down" : loadIcon("down.png"),
}
}

func Icon(name string) image.Image{
if icons == nil {
loadIcons()
}
return icons[name]
}

多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行的一致的基础上自由的重排访问顺序。loadIcons函数可能会被重拍为以下结果:

1
2
3
4
5
6
7
8
func loadIcons(){
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png"),
icons["up"] = loadIcon("up.png"),
icons["right"] = loadIcon("right.png"),
icons["down"] = loadIcon("down.png"),
}
}

这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。在这种情况下,我们会想到使用互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样又回引发性能问题。

使用sync.Once改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons(){
icons = map[string]image.Image{
"left" : loadIcon("left.png"),
"up" : loadIcon("up.png"),
"right" : loadIcon("right.png"),
"down" : loadIcon("down.png"),
}
}

func Icon(name string) image.Image{
loadIconsOnce.Do(loadIcons)
return icons[name]
}

并发安全下的单例模式

sync.Once实现单例

1
2
3
4
5
6
7
8
9
10
11
type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton{
once.Do(func() {
instance = &singleton{}
})
return instance
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

sync.Map

Go语言内置的map不是并发安全的。

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
//普通map在并发情况下是不安全的
//fatal error: concurrent map writes
var m = make(map[string]int)

func main() {
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=%v, v:=%v\n", key, get(key) )
wg.Done()
}(i)
}
wg.Wait()
}

func set(key string, value int){
m[key] = value
}

func get(key string) int {
return m[key]
}

并发执行会报错:fatal error: concurrent map writes

在并发情况下就需要使用为map加索保证并发的安全性,Go语言的sync包中提供了一个并发安全的sync.Map,并且内置StoreLoadDeleteRange等操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//使用并发安全的map
//sync.Map
var m = sync.Map{}

func main() {
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=%v, v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

原子操作

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时,代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁好。

atomic

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//定义接口
type Counter interface {
Inc()
Load() int64
}

//普通版
type CommonCounter struct {
counter int64
}

func (c *CommonCounter) Inc(){
c.counter++
}

func (c CommonCounter) Load() int64{
return c.counter
}

//互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}

func (m *MutexCounter) Inc(){
m.lock.Lock()
m.counter++
m.lock.Unlock()
}

func (m *MutexCounter) Load() int64{
return m.counter
}

//原子操作版
type AtomicCounter struct {
counter int64
}

func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}

func test(c Counter){
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
c.Inc()
wg.Done()
}(i)
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}

func main() {
//并发非安全版本
c1 := CommonCounter{}
test(&c1)
//使用互斥锁实现并发安全
c2 := MutexCounter{}
test(&c2)
//使用原子操作实现并发安全
c3 := AtomicCounter{}
test(&c3)
}