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

14.1 并发、并行和协程

一个应用程序时运行在机器上的一个进程,如QQ.exe,可以比作一个公司;进程是一个运行在自己内存地址空间的独立执行体。一个进程有一个或多个系统线程组成,这些线程其实是共享这块内存地址空间的,一起工作的执行体,可以比作一个个员工。一个并发程序可以在一个处理器或内核上,使用多个线程来执行任务,但是只有一个程序在某个时间点同时运行在多核或多处理器上,才是真正的并行。一个个员工在同时进行不同的任务,这个公司才是在并行运作。如果是一个员工干完一点,另一个接手,干完一点再接手,这样是并发,通过切换时间片来实现”同时运作“。
所以并发程序可以是并行的,也可以不是。
使用多线程的应用难以做到准确,最主要的问题是内存中数据共享,它们会被多线程以无法预知的方式,导致一些无法重现或随机的结果:称作竟态
解决办法在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以改变数据,完了再释放锁。Go的标准库sync就是实现加锁的。但是这样会给程序带来更低的性能,这个方法不再适合现代多核多处理器的编程。
在Go中,应用程序并发处理的部分叫做goroutine(协程),协程和系统线程不是一对一的关系,协程是根据一个或多个线程的可用性,映射(多路复用,执行于)它们之上的。
协程工作在相同的地址空间,共享内存的方式一定是同步的,Go使用channel来同步协程。
协程是轻量级的,比线程更轻,使用的内存更少。
协程通过go关键词调用一个函数来实现。
Go程序中的main函数也可以看作是一个协程,主协程。

  1. package main
  2. //计算100万的累加
  3. func test() {
  4. a := 0
  5. for i := 0; i < 1000000; i++ {
  6. a += i
  7. }
  8. }
  9. func main() {
  10. go test() //启动一个协程
  11. }

14.2 协程通信

上面的例子中,启动了一个协程去计算100万的累加,但是输出结果是空的。因为main函数是主协程,main函数执行完了,程序退出,子协程还没执行完,所以没有输出结果。
所以需要协程之间进行通信,实现所有子协程执行完了,主协程再退出。
有两种办法实现:等待组和通道。

14.2.1 Sync.WaitGroup

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. //计算100万的累加
  7. func test(wg *sync.WaitGroup) {
  8. a := 0
  9. for i := 0; i < 1000000; i++ {
  10. a += i
  11. }
  12. fmt.Println(a)
  13. wg.Done() //减1操作
  14. }
  15. func main() {
  16. var wg sync.WaitGroup //声明变量
  17. wg.Add(1) //加1操作
  18. go test(&wg) //启动一个协程
  19. wg.Wait() //等待wg的值为0才退出
  20. }

wg.wait()会一直阻塞,直到wg为0。而每个goroutine执行完都会使得wg减1,这样实现协程同步。

14.2.2 channel

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. //计算100万的累加
  6. func test(c chan int) {
  7. a := 0
  8. for i := 0; i < 1000000; i++ {
  9. a += i
  10. }
  11. fmt.Println(a) //499999500000
  12. c <- 1 //往通道写
  13. }
  14. func main() {
  15. c := make(chan int) //初始化一个通道变量
  16. go test(c) //启动一个协程
  17. <-c //读通道数据,读不到就阻塞
  18. }

<-c 是读通道,如果读不到数据,就会阻塞,知道读到数据为止。这样实现协程同步。
上面的例子是无缓冲通道,在没读到数据之前是阻塞的,也就是同步的,还有带缓冲的通道。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. //计算100万的累加
  6. func test(c chan int) {
  7. a := 0
  8. for i := 0; i < 1000000; i++ {
  9. a += i
  10. }
  11. fmt.Println(a)
  12. c <- 1 //往通道写
  13. }
  14. func main() {
  15. c := make(chan int, 10) //初始化一个通道变量,缓冲为10
  16. for i := 0; i < 10; i++ { //启动10个goroutine
  17. go test(c)
  18. }
  19. for i := 0; i < 10; i++ { //有10个goroutine,就取10次通道
  20. <-c
  21. }
  22. }

主要后面一定要取10次通道,有多少个goroutine就要取多少次通道,这样才能保证同步,不然会死锁。因为这是带缓冲的通道,没填满缓冲值之前,是不会阻塞的。

14.2.3 Gomaxproc

这是Go利用多核多处理器提升效率的核心。使用很简单,直接加一句话。

  1. runtime.GOMAXPROCS(runtime.NumCPU())

里面可以填int值n,代表要使用多少核心,numcpu代表当前机器的核心数