1. 并发模型

四种主流并发模型:

  • 多进程。多进程是在操作系统层面进行并发的基本模型。同时也是开销最大的模型。比如某个Web服务器,它会有专门的进程负责端口监听和链接管理,还会有专门的进程负责运算等。这种方法的好处是简单,各个进程互不干扰,坏处是系统开销大
  • 多线程。多线程在大部分操作系统上都属于系统层面的并发模式,也是我们使用最多的最有效的一种模式。它比多进程的开销小很多,但是其开销依旧比较大,且在高并发模式下,效率会有影响。
  • 基于回调的非阻塞/异步IO。这种模式通过事件驱动的方式使用异步IO,使服务器持续运转,尽可能降低线程使用(node.js中就是使用该模式)。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对问题本身的反应不够自然。
  • 协程。协程(Coroutine)本质上是一种用户态线程,不需要操作系统进行抢占式调度,在真正的实现中寄存在线程中,因此,系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语法层面支持,否则需要用户在程序中自行实现调度器

两种并发模式:

  • 共享内存系统。线程之间通信只能采用共享内存的方式,为了保证共享内存的有效性,要加入锁等,来避免死锁或资源竞争。
  • 消息传递系统。对线程间共享状态的各种操作封装都被封装在线程之间传递的消息中,这通常要求:发送消息时对状态进行复制,并且在消息传递的边界上交出这个状态的所有权。从逻辑上来看,这个操作与共享内存系统中执行的原子更新操作相同(每个时刻只能有一个线程对资源进行更改)。从物理上来看,由于需要执行复制操作,所以大多数消息传递的实现性能并不优越,但线程中的状态管理工作通常会变得更为简单。

2. goroutine

Go语言在语言级别支持轻量级线程,叫goroutine。Go语言标准库提供的所有系统调用操作,都会出让CPU给其他goroutine(怎么理解?)。这让事情变得非常简单,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。

在函数前加go 关键字,这次调用就会在新的goroutine中并发执行。如果函数有返回值,会自动抛弃,所以一般把无返回的函数放在goroutine中执行。

  1. func Add(x, y int) {
  2. z := x + y
  3. fmt.Println(z)
  4. }
  5. func main() {
  6. for i := 0; i < 10; i++ {
  7. go Add(i, i)
  8. }
  9. time.Sleep(2 * time.Second)
  10. }

3. 并发通信

3.1 两种并发通信模式

  • 共享数据
  • 消息传递

3.2 channel

Go语言中提供的消息通信机制称为channel。我们可以使用channel在两个或多个goroutine之间传递消息。
channel是类型相关的,也就是说一个channel只能传递一种类型的值,这个类型需要在声明channel时指定的。

  1. // 使用channel代替锁,保证count原子更新的有效性
  2. var count int
  3. func Add(ch chan) {
  4. c := <-ch
  5. count = c + 1
  6. fmt.Println(count)
  7. ch <- count
  8. }
  9. func main() {
  10. // 声明一个消息类型为int的channel
  11. ch := make(chan, int)
  12. for i := 0; i < 10000; i++ {
  13. go Add(ch)
  14. }
  15. ch <- count
  16. time.Sleep(3 * time.Second)
  17. }

3.3 select

Go语言直接在语言级别支持select关键字,用于处理异步IO问题。select中每个case语句都必须是一个面向channel的操作。

  1. // 随机往ch写入0或1
  2. func main() {
  3. // 对于需要持续传输数据的场景,我们创建带缓冲的channel
  4. ch := make(chan int, 1)
  5. for {
  6. select {
  7. // ch带缓冲,不会阻塞
  8. case ch <- 0:
  9. case ch <- 1:
  10. }
  11. i := <-ch
  12. fmt.Println("Value received:", i)
  13. }
  14. }

3.4 超时机制

在并发编程的通信过程中,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者从channel试图读取数据时发现channel为空。如果不正确处理这些情况,很可能会导致整个goroutine锁死。
通过channel+select实现超时机制

  1. ch := make(chan int, 1)
  2. timeout := make(chan bool, 1)
  3. go func(){
  4. // 等待1秒
  5. time.Sleep(1 * time.Second)
  6. timeout <- true
  7. }
  8. select {
  9. case i := <-ch:
  10. // 处理i
  11. case <-timeout:
  12. // 针对ch超时做处理,比如关闭ch
  13. close(ch)
  14. }

3.5 单向channel

单向channel即对channel做限制,只能使用与发送或接收。由于channel是原生类型,可以通过类型转换来创建单向channel

  1. ch := make(chan int, 1)
  2. // 只写channel
  3. ch1 := chan<- int(ch)
  4. // 只读channel
  5. ch2 := <-chan int(ch)
  6. ...
  7. func Parse(ch <-chan int) {
  8. for v := range ch {
  9. fmt.Println(v)
  10. }
  11. }

4. 总结

  • Go语言实现消息传递系统的并发模型,使用消息传递的并发模式
  • Go语言层面支持协程,实现调度器
  • Go中使用channel进行协程间的通信,多个channel一般搭配select使用,比如超时机制