Go 协程通信实现(上)—— 共享内存
在上篇教程中,我们已经演示了如何通过 goroutine 基于协程在 Go 语言中实现并发编程,从语法结构来说,Go 语言的协程是非常简单的,只需要通过 go
关键字声明即可,难点在于并发引起的不确定性,以及为了协调这种不确定性在不同协程间所要进行的通信,在并发开篇教程中,我们也介绍过在工程上,常见的并发通信模型有两种:共享内存和消息传递。
下面,我们先来看看如何通过共享内存来实现 Go 协程通信,并通过协程通信来重构上篇教程的代码,实现应用程序的优雅退出,新建一个 memory.go
,并编写代码如下:
package main
import (
"fmt"
"runtime"
"sync"
)
var counter int = 0
func add(a, b int, lock *sync.Mutex) {
c := a + b
lock.Lock()
counter++
fmt.Printf("%d: %d + %d = %d\n", counter, a, b, c)
lock.Unlock()
}
func main() {
start := time.Now()
lock := &sync.Mutex{}
for i := 0; i < 10; i++ {
go add(1, i, lock)
}
for {
lock.Lock()
c := counter
lock.Unlock()
runtime.Gosched()
if c >= 10 {
break
}
}
end := time.Now()
consume := end.Sub(start).Seconds()
fmt.Println("程序执行耗时(s):", consume)
}
为了精确判断主协程退出时机问题,我们需要在所有子协程执行完毕后通知主协程,主协程在收到该信号后退出程序,通过共享内存的方式我们引入了一个全局的 counter
计数器,该计数器被所有协程共享,每执行一次子协程,该计数器的值加 1,当所有子协程执行完毕后,计数器的值应该是 10,我们在主协程中通过一个死循环来判断 counter
的值,只有当它大于等于 10 时,才退出循环,进而退出整个程序。
此外,由于 counter
变量会被所有协程共享,为了避免 counter
值被污染(两个协程同时操作计数器),我们还引入了锁机制,即 sync.Mutex
,这是 Go 语言标准库提供的互斥锁,当一个 goroutine 调用其 Lock()
方法加锁后,其他 goroutine 必须等到这个 goroutine 调用同一个 sync.Mutex
的 Unlock()
方法解锁才能继续访问这个 sync.Mutex
(通过指针传递到子协程,所以整个应用持有的是同一个互斥锁),我们可以通过这种方式保证所有 lock.Lock()
与 lock.Unlock()
之间的代码是以同步阻塞方式串行执行的,从而保证对 counter
进行读取和更新操作时,同时只有一个协程在操作它(既保证了操作的原子性)。
最后,我们还统计了整个程序执行时间。
当我们执行这段代码时,打印结果如下:
可以看到,实际执行时间远远小于1秒,这样一来,程序的整体执行效率相比于上篇教程的实现快了将近1万倍。
不过,代码也因此变得更复杂,更难以维护,这还只是个简单的加法运算实现,就要写这么多代码,要引入共享变量,还要引入互斥锁来保证操作的原子性,对于更加复杂的业务代码,如果到处都要加锁、解锁,显然对开发者和维护者来说都是噩梦,Go 语言既然以并发编程作为语言的核心优势,当然不至于将这样的问题用这么繁琐的方式来解决。
前面我们说,除了共享内存之外,还可以通过消息传递来实现协程通信,Go 语言本身的编程哲学也是「Don’t communicate by sharing memory, share memory by communicating」,所以实际上,我们在 Go 语言并发编程实践中,使用的都是基于消息传递的方式实现协程之间的通信。
在消息传递机制中,每个协程是独立的个体,并且都有自己的变量,与共享内存不同的是,在不同协程间这些变量不共享,每个协程的输入和输出都只有一种方式,那就是消息,这有点类似于进程:每个进程都是独立的,不会被其他进程打扰,不同进程间靠消息来通信,它们不会共享内存。
下篇教程,我们就来系统介绍 Go 语言是如何基于消息传递实现协程间通信的。
6 Comments
这里少了一个包,time
runtime.Gosched() // 作用是什么?感觉没什么用啊?求解
有的电脑如果是单核 CPU,那么到
for
循环这里就会堵塞卡死,Gosched() 的作用是遇到堵塞情况交给同线程下的其他协程继续处理当前任务。可以设置成单核 CPU 处理就可以看见效果
add() 方法已经有 lock.Lock() 了, 为啥 main 里面的还要 lock.Lock() c := counter lock.Unlock()
因为add()里的counter和main的counter是同一个变量,add()和main()是并行运行的,所以在main中对counter进行处理的话也要加锁。这里并没有做什么处理,所以这里的锁没必要加。
学习打卡