GO语言并发
基本并发知识
并发和并行
- 并发:并发主要由切换时间片来实现宏观上的"同时"运行
- 并行:并行是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力
进程和线程
- 资源分配和调度:
- 进程是程序在操作系统中的一次执行过程,系统进行资源分配的一个独立单位。线程是进程的一个执行实体, 是CPU调度的基本单位,它是比进程更小的能独立运行的基本单位。
- 进程拥有独立的内存空间,同一个进程内部的多个线程共享进程的内存和资源。
- 开销:进程切换需要保存和恢复整个内存状态,开销较大;线程切换只需保存和恢复少量寄存器,开销较小。
- 独立性:进程之间互不影响,一个进程崩溃不会影响其他进程;线程共享进程资源,一个线程崩溃可能导致整个进程终止。
- 通信方式:进程间通信需要使用管道、消息队列、共享内存等机制;线程间可以直接通过共享内存进行通信,但需要同步机制保证数据一致性。
线程和协程
协程是一种用户态的轻量级线程,由程序自身控制调度,无需操作系统介入。它通过协作式多任务实现并发,适用于高并发和异步I/O场景。线程则是内核态的执行单元,由操作系统调度。
- 调度方式:协程采用用户态协作式调度,线程采用内核态抢占式调度。
- 切换开销:协程切换开销极低(通常小于1微秒),线程切换开销较高。
- 并发数量:单线程可管理数万个协程,而线程数量受限于内核资源(通常为数百个)。
- 使用场景:协程适合I/O密集型和高并发服务,线程适合CPU密集型和多核并行任务。
goroutine
goroutine其实就是一个超级大的线程池,或者说协程池。Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
一个简单的例子:
1 | var wg sync.WaitGroup |
注意
如果主协程退出了,其他任务还执行吗?
不会。
如果某个协程创建了一个子协程,那么这个协程退出后,其子协程还会执行吗?
runtime包
runtime.Gosched()
让出CPU时间片。将当前goroutine放回等待队列中,重新等待安排任务。
1 | package main |
runtime.Goexit()
Goexit() 函数用于终止当前Goroutine ,让当前 Goroutine 正常退出,但不影响其他 Goroutine 的运行。使用 Goexit() 函数时,可以在 Goroutine 内部调用,强制终止当前 Goroutine 的执行。
1 | package main |
runtime.GOMAXPROCS
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数,本质上runtime.GOMAXPROCS()控制的是GPM模型中的P的数量。
1 | func a() { |
将逻辑核心数改为2,程序会并行执行
1 | func a() { |
channel
GO语言提倡通过channel通信实现共享内存而不是通过共享内存实现通信。遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
Channel和select的搭配使用以及调度器对goroutine的调度,可以高效实现协程的阻塞和唤醒及多路复用。关于select的使用在后面。
基本使用
channel是一种引用类型,声明channel的举例:
1 | var chanInt chan int; // 声明一个传递int的通道 |
创建channel
1 | chanInt := make(chan int, 10) // 创建一个传递 int 的通道,缓冲区大小为10 |
channel的操作
channel有发送、接受、关闭(close)三种操作
1 | chInt := make(chan int) // 无缓冲channel |
上述代码不能直接执行,会报错:
1 | package main |
报错:
1 | fatal error: all goroutines are asleep - deadlock! |
channel的分类
无缓冲channel
- 当发送方执行 ch <- val 发送数据时,它会一直阻塞,直到有另一个 goroutine 执行<-ch 来接收数据。
- 反之,如果接收方先执行<-ch获取数据,它也会等待,直到发送方准备好数据。
无缓冲 Channel的行为类似于一种“同步握手”机制。这种严格的同步特性使得无缓冲 Channel 非常适合用于精确协调 goroutine 的执行顺序,例如确保某个任务完成后才允许后续操作继续执行。
无缓冲channel必须先启动接收方。否则发送方发送时会阻塞,如果此时接收方未启动,程序将死锁。
有缓冲channel
- 发送方在缓冲区未满时立即发送,而不必等待接收方。只有当缓冲区填满后,发送操作才会阻塞。
- 接收方在缓冲区为空时会等待,否则直接从缓冲区读取数据。
有缓冲的 Channel 底层使用环形队列。因为存在缓冲区,更适合处理突发流量或解耦生产者和消费者的执行速度,从而提高整体吞吐量。
channel的使用案例
无缓冲channel:
1 | package main |
有缓冲channel:
1 | package main |
select语句
类似与switch语句,专门用于操作channel,它会一直等待某个channel操作准备就绪,然后执行相应的case分支。如果多个case同时准备就绪,则会随机选择一个分支执行。为什么随机呢?因为如果每一次都按照顺序执行,则会导致每次都只执行第一个,造成其他case语句饥饿。
基本示例:
1 | package main |
上述程序只能打印20条消息的10条,因为selece语句每次只能随机二选一。
空select
空 select 是指没有任何 case 分支的 select 语句。这种写法会造成 goroutine 永远阻塞,常用于阻塞主 goroutine 以防止程序退出。
1 | package main |
只有一个case分支的select
1 | package main |
含有default分支的select
在select语句中加入default分支,用于在没有任何channel就绪时执默认操作。这样可以避免阻塞。适用于需要非阻塞处理的场景。
1 | package main |
超时控制
1 | select { |
time.After是Go标准库time包中的一个函数,它返回一个Channel,该Channel会在指定时间后发送一个时间值。
- 如果
ch在3秒内有数据,case <-ch:先执行 - 如果
ch在3秒内没有数据,case <-time.After(3 * time.Second):先执行