golang并发编程

409 阅读5分钟

1. go 并发基础

golang并发是携程,使用关键字 go 可以开启一个协程

  • go(协程) : 通过关键字 go 即可创建一个协程.
  • chan :  golang 中用于并发的通道,用于协程的通信.
    • 有缓冲通道
    • 无缓冲通道
    • 单向通道
  • select: golang 提供的多路复用机制.
  • close() : golang 的内置函数, 可以关闭一个通道.
  • sync: golang 标准库之一,提供了锁.
  • 定时器: golang 标准库 time 提供的重要功能, 提供了定时器功能,可用于超时处理.
    • Timer
    • Ticker

2. go 并发编程

go 的并发编程采用的 CSP (Communicating Sequential Process) 模型,主要基于协程 goroutine 和通道 channel .

2.1 协程 go

在 go 语言中,并发编程使用关键字 go 即可快速启动一个并发运行的 goroutine. 如下:

go 函数名 (参数列表)
go f(a int, b int, c int){
  fmt.Println(a+b+c)
}(1,2,3)

2.2 channel 通道

golang 提供了通道类型 chan,用于在并发操作时的通信,它本身就是并发安全的. 通过 chan 可以创建无缓冲、缓冲通道、单向通道,满足不同需求. 写法如下:

make(chan int)
make(chan int, 10)
<- chan
chan <-

无缓冲通道:  要求接受和发送数据的 goroutine 同时准备好,否则将会阻塞.

有缓冲通道:  给予通道一个容量值,只要有值便可以接受数据,有空间便可以发送数据,可以不阻塞的完成.

单向通道: 默认情况通道是双向的,可以接受及发送数据. 也可以创建单向通道,只能收或者发数据. 如下是单向接受通道

var ch chan
  <- float64

2.3 select

select:  可以监听 channel 上的输入/输出操作, 类似于 select、epoll、poll 使得通道支持多路复用. select 是专门通道 channel 设计的. 它可以结合通道实现超时处理、判断缓冲通道是否阻塞、退出信号量处理,如下:

// 1. 超时机制
select {
  case <-ch:
  case <-timeout:
  fmt.Println("timeout 01")}​// 2. 退出信号量处理select {    case <- quitChan:        return    default:}​// 3. 判断缓冲通道是否已满ch := make(chan int, 5)ch <- 1select {    case ch <- 2:        fmt.Println("channel value is", <-ch)    default:        fmt.Println("channel blocking")}

2.4 内置函数 close():

close(): 用于关闭通道 channel 的,close 之后的 channel 还可以读取数据,close() 函数由以下几点使用要点:

  1. 只能关闭双向通道或者发送通道
  2. 它应该由发送者使用,而不应该由接受者调用
  3. 当通道关闭后,接受者都不再阻塞,
  4. 关闭通道后,依然可以从通道中读取值
  5. 所有元素读取完后,将返回通道元素的零值,并且读取检测值也是 false

示例:

ch := make(chan int, 1)
ch <- 3
close(ch) // 关闭ch
v, ok := <- ch // 3,true
v2,ok := <- ch // 0,false

3. 阻塞、同步与异步

3.1 阻塞与非阻塞

进程状态

阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当数据未准备就绪,这个进程(线程、协程)一直等待,这就是阻塞.

非阻塞: 当数据为准备就绪,该进程(线程、协程)不等待可以继续执行,这就是非阻塞.

3.2 同步与异步

同步: 在发起一个调用时,在没有得到结果之前,这个调用就不返回,这个调用过程一直在等待. 这是同步.

异步: 在发起调用后,就立刻返回了,这次调用过程就结束了. 等到有结果了被调用方主动通知调用者结果. 这是异步.

3.3 四种组合

同步、异步、阻塞、非阻塞可以组合成四种并发方式:

  • 同步阻塞调用:得不到结果不返回,线程进入阻塞态等待。
  • 同步非阻塞调用:得不到结果不返回,线程不阻塞一直在CPU运行。
  • 异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞。
  • 异步非阻塞调用:去到别的线程,别的线程一直在运行,直到得出结果。

4. golang 标准库的锁与定时器

4.1 锁与 sync 库

并发编程中,为了确保并发安全,可以使用锁机制. golang 提供了标准库 sync ,它实现了并发需要的各种锁. 包括:

  • Mutex: 互斥锁,有俩个方法 Lock()  和 Unlock() , 它只能同时被一个 goroutine 锁定,其它锁再次尝试锁定将被阻塞,直到解锁.
  • RWMutex: 读写锁,有四个方法,Lock() 写锁定、Unlock() 写解锁、RLock() 读锁定、RUnlock() 读解锁,读锁定和写锁定只能同时存在一个. 只能有一个协程处于写锁定状态,但是可以有多个协程处于读锁定状态. 即写的时候不可读,读的时候不可写. 只能同时有一个写操作确保数据一致性. 而可以多个协程同时读数据,确保读操作的并发性能.

此外在 go 的并发编程中,还会常用到 sync 的以下内容:

  • sync.Map: 并发安全的字典 map
  • sync.WaitGroup: 用来等待一组协程的结束,常常用来阻塞主线程.
  • sync.Once: 用于控制函数只能被使用一次,
  • sync.Cond: 条件同步变量. 可以通过 Wait()方法阻塞协程,通过 Signal()、Broadcast() 方法唤醒协程.
  • sync.Pool: 一组临时对象的集合,是并发安全的. 它主要是用于存储分配但还未被使用的值,避免频繁的重新分配内存,减少 gc 的压力.

3.2 time 库的定时器

golang 的标准库 time 中提供了定时器功能,并提供通道 channel 变量进行定时通知. time 库中提供了两种定时器:

  • time.Timer:  定时器 timer 在创建指定时间后,向通道 time.Timer.C 发送数据. 之后需要使用 Reset 设定定时器时间.
  • time.Ticker:  周期性定时器. 会按照初设定的时间重复计时.