14.1 并发、并行和协程
一个应用程序时运行在机器上的一个进程,如QQ.exe,可以比作一个公司;进程是一个运行在自己内存地址空间的独立执行体。一个进程有一个或多个系统线程组成,这些线程其实是共享这块内存地址空间的,一起工作的执行体,可以比作一个个员工。一个并发程序可以在一个处理器或内核上,使用多个线程来执行任务,但是只有一个程序在某个时间点同时运行在多核或多处理器上,才是真正的并行。一个个员工在同时进行不同的任务,这个公司才是在并行运作。如果是一个员工干完一点,另一个接手,干完一点再接手,这样是并发,通过切换时间片来实现”同时运作“。
所以并发程序可以是并行的,也可以不是。
使用多线程的应用难以做到准确,最主要的问题是内存中数据共享,它们会被多线程以无法预知的方式,导致一些无法重现或随机的结果:称作竟态。
解决办法在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以改变数据,完了再释放锁。Go的标准库sync就是实现加锁的。但是这样会给程序带来更低的性能,这个方法不再适合现代多核多处理器的编程。
在Go中,应用程序并发处理的部分叫做goroutine(协程),协程和系统线程不是一对一的关系,协程是根据一个或多个线程的可用性,映射(多路复用,执行于)它们之上的。
协程工作在相同的地址空间,共享内存的方式一定是同步的,Go使用channel来同步协程。
协程是轻量级的,比线程更轻,使用的内存更少。
协程通过go关键词调用一个函数来实现。
Go程序中的main函数也可以看作是一个协程,主协程。
package main
//计算100万的累加
func test() {
a := 0
for i := 0; i < 1000000; i++ {
a += i
}
}
func main() {
go test() //启动一个协程
}
14.2 协程通信
上面的例子中,启动了一个协程去计算100万的累加,但是输出结果是空的。因为main函数是主协程,main函数执行完了,程序退出,子协程还没执行完,所以没有输出结果。
所以需要协程之间进行通信,实现所有子协程执行完了,主协程再退出。
有两种办法实现:等待组和通道。
14.2.1 Sync.WaitGroup
package main
import (
"fmt"
"sync"
)
//计算100万的累加
func test(wg *sync.WaitGroup) {
a := 0
for i := 0; i < 1000000; i++ {
a += i
}
fmt.Println(a)
wg.Done() //减1操作
}
func main() {
var wg sync.WaitGroup //声明变量
wg.Add(1) //加1操作
go test(&wg) //启动一个协程
wg.Wait() //等待wg的值为0才退出
}
wg.wait()会一直阻塞,直到wg为0。而每个goroutine执行完都会使得wg减1,这样实现协程同步。
14.2.2 channel
package main
import (
"fmt"
)
//计算100万的累加
func test(c chan int) {
a := 0
for i := 0; i < 1000000; i++ {
a += i
}
fmt.Println(a) //499999500000
c <- 1 //往通道写
}
func main() {
c := make(chan int) //初始化一个通道变量
go test(c) //启动一个协程
<-c //读通道数据,读不到就阻塞
}
<-c 是读通道,如果读不到数据,就会阻塞,知道读到数据为止。这样实现协程同步。
上面的例子是无缓冲通道,在没读到数据之前是阻塞的,也就是同步的,还有带缓冲的通道。
package main
import (
"fmt"
)
//计算100万的累加
func test(c chan int) {
a := 0
for i := 0; i < 1000000; i++ {
a += i
}
fmt.Println(a)
c <- 1 //往通道写
}
func main() {
c := make(chan int, 10) //初始化一个通道变量,缓冲为10
for i := 0; i < 10; i++ { //启动10个goroutine
go test(c)
}
for i := 0; i < 10; i++ { //有10个goroutine,就取10次通道
<-c
}
}
主要后面一定要取10次通道,有多少个goroutine就要取多少次通道,这样才能保证同步,不然会死锁。因为这是带缓冲的通道,没填满缓冲值之前,是不会阻塞的。
14.2.3 Gomaxproc
这是Go利用多核多处理器提升效率的核心。使用很简单,直接加一句话。
runtime.GOMAXPROCS(runtime.NumCPU())
里面可以填int值n,代表要使用多少核心,numcpu代表当前机器的核心数