并发 vs. 并行
- 并发:多线程程序通过切换在多个任务间进行。
- 并行:多线程程序在多个核心的CPU上分别同时运行。
Go 可以充分发挥多核优势,高效运行。
Go语言中的并发程序可以用两种手段来实现。其一是goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。CSP是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中。另一种是更为传统的并发模型:多线程共享内存。
Goroutines
在Go语言中,每一个并发的执行单元叫作一个goroutine。可以简单地把goroutine类比作一个线程。但与线程不同的是,线程指的是操作系统调度到CPU中执行的基本单位,例如在浏览器里新建一个窗口就需要一个线程来进行处理,其栈大小为MB级别。而goroutine是从属于某一个线程的,由Go语言运行时的调度器进行调度,相当于用户态的轻量级线程,其栈大小只有KB级别。
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。
f() // 调用f(); 等待其返回
go f() // 创建一个名为f()的goroutine;不需要等待返回
例如,下面这个例子通过一个名为spinner()的goroutine来实现一个加载动画。
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
主函数返回时,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过goroutine之间的通信来让一个goroutine请求其它的goroutine,并让被请求的goroutine自行结束执行。
CSP (Communicating Sequential Processes)
CSP用于描述两个独立的并发实体通过共享的通讯channel进行通信的并发模型。它提倡通过通信共享内存而不是通过共享内存。在Go中,它的process就是goroutine,而它使用channel来作为goroutines之间的通信机制。
Channels
一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。使用内置的make函数,我们可以创建一个channel。
语法:make(chan TYPE, BUFFER_SIZE)
make(chan int) // 创建了一个可以发送int类型数据的channel;无缓冲通道
make(chan int, 2) // 有缓冲通道
一个Channel有发送和接收(生产和消费)两个主要操作,都是通信行为。
ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; resuilt is discarded
要关闭一个channel,可以使用内置的close函数。
close(ch)
例如在下面这个例子中,close(naturals)会关闭naturals这个channel。而要检验一个channel是否被关闭,可以让接收操作多接收一个状态值,true表示成功从channel接收到了值,而false表示channel已经被关闭。
// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * x
}
close(squares)
}() // DUMB
// gopl.io/ch8/pipeline2
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
Go中还有一个defer关键字同样可以配合close函数用于关闭对应的channel。defer关键字用于声明一个延迟函数,一般称作defer函数,它会在defer语句所在函数返回前执行。它可以放在函数中中的任意位置,而有多个defer函数时,它们的执行遵循FILO顺序。
例如下面的例子中,defer close(ch)的作用和上面一个例子类似。
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
并发安全与互斥锁 sync.Mutex
在一个有多个goroutine的程序中,当分别位于两个goroutine的两个事件的执行顺序无法判断时,我们就认为这两个事件是并发的。对于某个类型来说,如果其所有可访问的方法和操作都是并发安全的话,那么该类型便是并发安全的。并发可能会存在多个goroutine同时操作一个资源,而这种状态下会发生竞争,最终可能导致程序最后的结果与期待不符。
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock() // 加锁,保证此时只有一个goroutine可以访问x
x += 1
lock.Unlock() // 解锁
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x)
}
在上面这个例子中,我们可能会看到以下输出结果。
WithLock: 10000
WithoutLock: 8382
我们还可以使用读写锁(sync.RWMutex)使并发安全。
sync.WaitGroup
如果有多个goroutines需要同时执行,然后执行下一步操作这样的场景,适合使用 sync.WaitGroup。
WaitGroup下有几个函数。其中Add(delta int)用于将计数器+delta,Done函数可以让计数器-1,而Wait能使主协程阻塞直到计数器为0.
不适用WaitGroup进行快速打印时,我们可能这样写:
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
而用WaitGroup实现协程的同步阻塞,我们可以这样写:
func ManyGo() {
var wg sync.WaitGroup
wg.Add(5) // 计数器+5
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() //func()每次返回前对计数器-1
hello(j)
}(i)
}
wg.Wait() // 阻塞主协程直到计数器为0
}
部分引用自Go语言圣经