在开始 Go 语言并发编程之前,我们先简单地了解一些概念:
- 1、进程和线程:
- 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个执行实例,是 CPU 调度和分派的基本单位。
- 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
- 2、协程和线程:
- 协程,独立的栈空间,共享堆空间,调度由用户自己控制。
- 线程,在一个线程上可以跑多个协程,就是说协程是轻量级的线程。
- 3、并发和并行:
-
多线程程序在一个核心的 CPU 上运行,就是并发,并发主要由切换时间片来实现"同时"运行。
-
多线程程序在多个核心的 CPU 上运行,就是并行,并行是直接利用多核实现多线程的运行。
-
Go 语言中的并发其实就是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine
时,Go 会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。
1.goroutine
golang runtime GPM 调度机制
调度器历史
(1)单进程时代
早期的操作系统,每个程序就是一个进程,只有当一个程序运行完毕才能进行下一个进程。当然这样的系统并不高效,我们都直到它的缺点:
- 只能一个一个任务处理,速度很慢;
- 如果有进程被阻塞在读磁盘文件的时候,CPU 就完全空闲,而后面的任务只能干等。
(2)多进程/线程时代
在多进程/线程的操作系统中,把每个任务都切成好几片,用一个 CPU 调度器去管理。这样做的好处是,当其中一个进程阻塞的时候,我们可以安排其他进行去执行,这样我们的 CPU ‘帕鲁’ 就能不间断地干活了。
但是,聪明的我们又发现问题了:每个进程的创建、切换、销毁等等也是占用时间的,要是任务特别多的话,CPU 就又要花很多时间在处理进程的调度,并没有在正在地干活。
(3)协程时代
线程其实分有“内核态线程”和“用户态线程”,而且一个“用户态线程”必须要绑定一个“内核态线程”。
话是这么说的,但实际上 CPU 并不真知道“用户态线程”,它只明白它要运行的是一个“内核态线程”,在 Linux 中就是一个进程控制模块,PCB。
于是,我们就想能不能让多个“用户态线程”绑定一个“内核态线程”呢?
所以,我们就有了协程(co-routine),协程就是“用户态线程”,而线程(thread) 就是“内核态线程”。
协程通过协程调度器给到线程,而线程通过 CPU 调度器给到 CPU 去执行。协程的调度是协作式的,就是只有当一个协程选择放弃对 CPU 的控制时,才会被切换;而线程的调度是抢占式的,如果有一个线程比当前执行的线程优先级高,那么当前线程就会被掐断,要乖乖把 CPU 让给优先级高的线程去执行。
这里可以根据协程与线程的映射关系,可以分为:N对1、1对1、N对M。
N对1的情况:N个协程与1个线程绑定,好处是协程之间的切换不用切换到内核态去,缺点就是阻塞后其他协程只能等待。
1对1的情况:实际就像是线程的执行,完全没有协程的优势了。
N对M的情况:这种情况是比较好的,但是怎么去调度就是一个问题了。
Goroutine 调度器
现在,我们对协程、线程和调度器有一定的认识了。接下来,在学习 Go 的协程调度之前,我们先看看线程池。
线程池是这样设计的,我们把每个 任务G(通常是个函数,注意这不是协程)发布到任务队列中,然后让线程池中的 线程woker 去把任务从队列中取出执行,当然 线程woker 的调度是由操作系统去做的。
通过线程池,我们规定只创建一定数量的线程,每个线程一次携带一个任务去执行,执行完后再回来携带另一个任务。
因为线程是固定数量的,要是这些线程都被阻塞,那么整个任务队列就会停摆。当然我们也可以增加线程池中的线程数量,但是数量多的话,就会由很多个线程去争抢 CPU,反而不一定能提高消费能力。
那么,我们看一下 Go 是怎么做的吧。首先我们要了解一些概念:
- G(Goroutine):就是 Go 协程,每个 go 关键字都会创建一个协程。
- M(Machine):工作线程,在 Go 中称为 Machine。
- P(Processor):逻辑处理器,包含运行 Go 代码的必要资源,也由调度 goroutine 的能力。
规则是:每个工作线程(M)需要协程(G)才能执行,而且逻辑处理器§有一个包含多个协程(G)的队列,然后逻辑处理器§可以调度协程(G)给工作线程(M)执行。
逻辑处理器§的个数是在程序启动时决定的,默认情况下等同于 CPU 的核数,而且一个逻辑处理器§只绑定一个工作线程(M),所以线程个数默认等同于 CPU 的个数。
Goroutine 调度策略
我们知道了 Goroutine 的调度是通过逻辑处理器§去调度的,那么具体是怎么做的呢?
队列轮转
在逻辑处理器§自己维护的队列上,如果协程(G)没有被阻塞,那么就将其执行一段时间后终止,将上下文保存下来后放到队列的尾部,然后从队列中重新取出一个协程(G)进行调度。
除了每个逻辑处理器§上的协程(G)队列外,运行时还有一个全局的队列。全局的队列是用来收留那些从系统调用中回复的协程(G)的。
所以,每个逻辑处理器§除了要调度自己的队列外,还要看看全局队列中是否有协程(G),有的话就把它调度到自己维护的队列上。
系统调用
那么发生系统中断的时候会发生的事情就是,逻辑处理器§会将当前这个工作线程(M)与当前执行的协程(G)脱离。
脱离的线程(M)就在一遍等待系统的调用,而逻辑处理器§会新建立一个线程,继续执行它自己维护队列里的协程(G)。
工作量窃取
当某个逻辑处理器§自己队列上的协程(G)都处理完了后,除了去看全局队列,它还会去看别的逻辑处理器§是否把它们的协程(G)都处理完了。
如果发现其他逻辑处理器§的队列上还有协程(G),那么它就会窃取一部分协程(G)过来执行,一般每次偷取一半。
一般来讲,程序运行时就将 GOMAXPROCS 大小设置为 CPU 核数,可让 Go 程序充分利用 CPU。在某些 IO 密集型的应用里,这个值可能并不意味着性能最好。理论上当某个 Goroutine 进入系统调用时,会有一个新的 M 被启用或创建,继续占满 CPU 、。但由于 Go 调度器检测到 M 被阻塞是有一定延迟的,也即旧的 M 被阻塞和新的 M 得到运行之间是有一定间隔的,所以在 IO 密集型应用中不妨把 GOMAXPROCS 设置的大一些,或许会有好的效果。
goroutine 同步与通信
Go 的运行时(runtime)是 Go 的核心部分,负责管理内存分配、垃圾回收、并发调度等底层任务。
我们的协程 goroutine 是由运行时管理的,就连 main 函数也都是运行在 goroutine 上的。
package main
import (
"fmt"
"time"
)
func test(){
fmt.Println("I am work in a single goroutine")
}
func main(){
// 把函数 test() 给到 goroutine
go test()
// 在 main 的 goroutine 上休眠一下
time.Sleep(time.Second)
}
上面的代码,假设我们不进行休眠的话,那么 main 执行完 go test()
就会马上结束,而 go test()
可能还没开始执行,所以可能什么都没返回。
此外,和其他编程语言一样,Go 不同 goroutine 间的代码次序并不能代表真正的执行顺序。
package main
import "fmt"
func setVTo1(v *int){
*v = 1
}
func setVTo2(v *int){
*v = 2
}
func main(){
v := new(int)
go setVTo1(v)
go setVTo2(v)
fmt.Println(*v)
}
上面代码的执行结果可能为 0、1或2,但最有可能的情况是 0,这取决于调度器的调度情况。
为了能够按照我们想要的顺序去执行 goroutine ,就需要用到 同步与通信 的一些方法去执行并发:
- channel:提供一种安全且同步的方式来发送和接收值。
- select:用于同时等待多个
channel
操作,处理第一个可用的channel
。 - sync.WaitGroup:提供一种简单的方法来等待一组 goroutine 完成执行。
- sync.Mutex:提供了一种锁机制来保护共享资源,防止并发访问导致的数据竞争。
- sync.RWMutext:读写锁,允许多个读操作同时进行,但写操作需要独占锁。
2.channel
Go 语言中倡导用 channel 作为 goroutine 之间同步和通信的手段。
channel 类型属于引用类型,而且每个 channel 只能传递固定类型的数据。
// 声明 channel 并指定传递类型为 T
var channelName chan T
// make 初始化 channel,sizeOfChan 为缓冲
ch := make(chan T, sizeOfChan)
其中,默认不带缓冲的 channel 也叫 同步 channel,收发数据的操作都会被阻塞;带缓冲的 channel 叫 异步 channel 收发数据时,如果 channel 未达到阻塞条件则不会被阻塞。
channel 的发送与接收
channel 作为一个队列,它总是保证数据收发顺序总是遵循先入先出的原则,同时也保证同一时刻只有一个 goroutine 访问 channel 来发送和获取数据。
此外,如果不用 defer
语句关闭 channel 或者用 sync.WaitGroup
的 Done
方法关闭 goroutine 的话,可能会导致 goroutine 泄露。
channel <- val
表示数据 val 将被发送到 channel 中;如果 channel 被填满之后,再向通道发送数据,则会阻塞当前的 goroutine。
val := <- channel
表示从 channel 中读取值并赋值到 val;如果 channel 没有数据,那么就阻塞要读取数据的 goroutine 直到有数据进入 channel。
如果想在读 channel 时返回,可以写成 val, ok := <- channel
只要检查一下 ok
就能判断是否读到了有效数据。
package main
import (
"bufio"
"fmt"
"os"
)
func printInput(ch chan string){
// for 循环从 channel 中读取数据
for val := range ch{
if val == "EOF"{
break
}
fmt.Printf("Input is %s \n", val)
}
}
func main(){
// 无缓冲 channel
ch := make(chan string)
// 启动协程,因为 chan 无数据先被阻塞
go printInput(ch)
// 从命令行读取输入发给 goroutine
scanner := bufio.NewScanner(os.Stdin)
// 阻塞,等待命令行输入
for scanner.Scan(){
val := scanner.Text()
ch <- val
if val == "EOF"{
fmt.Println("End the game!")
break
}
}
// 最后关闭 chan
defer close(ch)
}
带缓冲的 channel
如果创建 channel 时指定了 channel 长度,那么 channel 就会拥有缓冲区,goroutine 在缓冲区未填满时往 channel 发送数据将不会被阻塞。
package main
import (
"fmt"
"time"
)
func consume(ch chan int){
// 线程休息 3s 后再开始读数据
time.Sleep(time.Second * 3)
<- ch
}
func main(){
// 长度为 2 的 channel
ch := make(chan int, 2)
// 启动线程
go consume(ch)
ch <- 0
ch <- 1
fmt.Println("I am free!")
ch <- 2 // 阻塞,等待 goroutine 读取数据
fmt.Println("I can not go there within 3s!")
time.Sleep(time.Second)
}
此外,我们可以声明单向的 channel,就是只能从 channel 发送数据或者只能从 channel 中读取数据,一般用于在传递 channel 时作为函数的参数,保证代码接口的严谨,避免进行越界操作。
ch := make(chan T)
// 声明只能发送的通道
var chInputOnly chan <- int = ch
// 声明只能接收的通道
var chOutputOnly int <- chan = ch
select 与 channel
如果 goroutine 中需要接收多个 channel 中的消息时,可以使用 Go 语言中的 select 提供的多路复用功能。
select 的使用方式与 switch 类似,但要求 case 语句后面必须为 I/O 操作,在没有 I/O 响应且没有提供 default 语句时,goroutine 会被阻塞。
package main
import (
"fmt"
"time"
)
func send(ch chan int, begin int){
for i := begin; i < begin + 10; i++{
ch <- i
}
}
func main(){
ch1 := make(chan int)
ch2 := make(chan int)
go send(ch1, 0)
go send(ch2, 10)
// 休眠,保证管道有数据
time.Sleep(time.Second)
for {
select{
case val := <- ch1:
fmt.Printf("Get value %d from ch1\n", val)
case val := <- ch2:
fmt.Printf("Get value %d from ch2\n", val)
// 超时设置
case <-time.After(2*time.Second):
fmt.Println("Time out")
return
}
}
}
我们用 select 多路复用分别从 ch1 和 ch2 中读取数据,如果多个 case 语句中的 ch 同时到达,那么 select 会运行一个伪随机算法随机选择一个 case。
因为 channel 的阻塞时无法被中断的,因此通过超时设置来去中断返回是一个好用的技巧。
此外,要注意在使用 channel 时可能会发生的思索情况:
- 1、相互等待:两个或多个 goroutine 相互等待对方发送或接收数据,但都没有继续执行的操作。
- 2、向已关闭的 channel 发送数据:尝试向一个已经被关闭的 channel 发送数据,会导致运行时 panic。
- 3、所有 goroutine 都在等待:所有 goroutine 都在等待从某个 channel 接收数据,而没有其他 goroutine 会向这个 channel 发送数据。
3.sync
sync 提供了如下 7 种并发工具:
并发工具 | 说明 |
---|---|
Mutex | 互斥锁 |
RWMutex | 读写锁 |
WaitGroup | 并发等待组 |
Map | 并发安全字典 |
Cond | 同步等待条件 |
Once | 只执行一次 |
Pool | 临时对象池 |
Atomic | 原子操作 |
Mutex(互斥锁)
sync.Mutex
能够保证在同一个时间段内仅有一个 goroutine 持有锁,这就能保证在某个时间段内有且仅有一个 goroutine 访问共享资源,其他申请锁的 goroutine 将会被阻塞直到锁被释放。
// sync.Mutex 提供两种方法
Lock()
Unlock()
package main
import (
"fmt"
"sync"
"time"
)
func main(){
var lock sync.Mutex
go func(){
// 加锁
lock.Lock()
defer lock.Unlock()
fmt.Println("FUNC1 get lock at " + time.Now().String())
time.Sleep(time.Second)
fmt.Println("FUNC1 release lock " + time.Now().String())
}()
time.Sleep(time.Second / 10)
go func(){
// 加锁
lock.Lock()
defer lock.Unlock()
fmt.Println("FUNC2 get lock at " + time.Now().String())
time.Sleep(time.Second)
fmt.Println("FUNC2 release lock " + time.Now().String())
}()
// 等待所有 goroutine 执行完
time.Sleep(time.Second * 4)
}
RWMutex(读写锁)
sync.RWMutex
将读锁和写锁的操作分离开来,为了保证在同一时间段内能够有多个 goroutine 访问同一资源,它要满足一下条件:
- 同一时段,只能有一个 goroutine 获取写锁。
- 同一时段,可以有多个 goroutine 获取读锁。
- 同一时段,要么都是写锁,要么都是读锁(互斥)。
概括起来就是,只允许多读或单写。
// sync.RWMutex 提供以下接口
// 写加锁
func (rw *RWMutex) Lock()
// 写解锁
func (rw *RWMutex) Unlock()
// 读写锁
func (rw *RWMutex) RLock()
// 读解锁
func (rw *RWMutex) RUnlock()
下面代码运行之后,我们会发现所有的 read goroutine 可以同时申请到读锁,而申请写锁的 goroutine 则必须等到锁被释放后才能争抢。
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var rwLock sync.RWMutex
func main(){
// 获取读锁
for i := 0; i < 5; i++{
go func(i int){
rwLock.RLocker()
defer rwLock.RLocker()
fmt.Println("read func " + strconv.Itoa(i) + " get rlock at " + time.Now().String())
time.Sleep(time.Second)
}(i)
}
time.Sleep(time.Second / 10)
// 获取写锁
for i := 0; i < 5; i++{
go func(i int){
rwLock.Lock()
defer rwLock.Unlock()
fmt.Println("write func " + strconv.Itoa(i) + " get rlock at " + time.Now().String())
time.Sleep(time.Second)
}(i)
}
// 等待所有 goroutine 执行完
time.Sleep(time.Second * 4)
}
WaitGroup(并发等待组)
用 sync.WaitGroup
的 goroutine 会等待预设好数量的 goroutine 都执行提交结束后,才会继续往下执行代码。
// sync.WaitGroup 提供的接口
// 添加等待数量,传递负数表示任务减一
func (wg *WaitGroup) Add(delta int)
// 等待数量数量减一
func (wg *WaitGroup) Done()
// 让 goroutine 等待在这里
func (wg *WaitGroup) Wait()
在 goroutine 调用 waitGroup.Wait 进行等待之前,要保证 waitGroup 中等待数量大于 1,即 waitGroup.Add 方法需要在 waitGroup.Wait 之前执行,否则等待就会被忽略,此外需要保证 waitGroup.Done 执行次数与 waitGroup.Add 添加的等待数量一致,过少会导致等待 goroutine 死锁,过多会导致程序 panic。
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
func main(){
var waitGroup sync.WaitGroup
// 添加等待 goroutine 数量为 5
waitGroup.Add(5)
for i := 0; i < 5; i++{
go func(i int){
fmt.Println("work " + strconv.Itoa(i) + " is done at " + time.Now().String())
time.Sleep(time.Second)
waitGroup.Done()
}(i)
}
waitGroup.Wait()
fmt.Println("all works are done at " + time.Now().String())
}
Map(并发安全字典)
sync.Map
就是添加了同步控制的字典,Go 原生的字典并不是并发安全的,当多个 goroutine 同时往 Map 中添加数据时,可能会导致部分添加的数据丢失。
// sync.Map 提供以下接口
// 根据 key 获取存储值
func (m *Map) Load(keu interface{}) (value interface{}. ok bool)
// 设置 key_value 对
func (m *Map) Store(key, value interface{})
// 如果 key 存在则返回 key 对应的 value,否则设置 key-value 对
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
// 删除一个 key 以及对应的值
func (m *Map) Delete(key interface{})
// 无序遍历 map
func (m *Map) Range(f func(key, value interface{}) bool)
package main
import (
"fmt"
"strconv"
"sync"
)
var syncMap sync.Map
var waitGroup sync.WaitGroup
func main(){
countSize := 5
waitGroup.Add(countSize)
for i:=0; i<countSize; i++{
go func(begin int){
for j := begin; j < begin+3; j++{
syncMap.Store(j, j)
}
waitGroup.Done()
}(i*10)
}
waitGroup.Wait()
var size int
// 统计数量
syncMap.Range(func(key, value interface{}) bool{
size++
return true
})
fmt.Println("syncMap current size is " + strconv.Itoa(size))
// 获取键为 0 的值
value, ok := syncMap.Load(0)
if ok {
fmt.Println("Key 0 has value", value, " ")
}
}
Pool(临时对象池)
sync.Pool
是 Go 中的一个并发安全的对象池,用于存储和复用临时对象,从而减少内存分配和垃圾回收的压力。
sync.Pool
中的对象是可以被无通知地回收的,而且大小受内存大小限制。
sync.Pool
提供了以下接口:
- New:当
sync.Pool
中没有可用对象时,调用函数来创建新的对象。 - Get:从
sync.Pool
中获取一个对象。 - Put:将对象放回
sync.Pool
中,以便重复使用。
package main
import (
"fmt"
"sync"
)
func main() {
type Student struct{
id string,
name string,
age int
}
// 初始化 Pool 实例
var studentPool = sync.Pool{
New: func() interface{}{
return new(Student)
},
}
// Get() 从对象池中获取对象
// 因为返回的是接口,所以通过类型断言提取类型
stu := studentPool.Get().(*Student)
stu.name = "Mike"
// Put() 在对象使用完成后,返回对象池
studentPool.Put(stu)
}
Atomic(原子操作)
sync/atomic
原子操作是比其它同步技术更基础的操作。原子操作是无锁的,常常直接通过CPU指令直接实现。
对于一个整数类型 T
,sync/atomic
标准库包提供了下列原子操作函数。 其中 T
可以是内置 int32
、int64
、uint32
、uint64
和 uintptr
类型。
// 原子地将值加到指定地址
func AddT(addr *T, delta T)(new T)
// 原子地加载值
func LoadT(addr *T) (val T)
// 原子地存储值
func StoreT(addr *T, val T)
// 原子地交换值
func SwapT(addr *T, new T) (old T)
// 原子地比较指定地址值,如果相等就交换
func CompareAndSwapT(addr *T, old, new T) (swapped bool)
sync/atomic
包在 Go 语言中有广泛的应用,特别是在需要高效并发控制的场景下。以下是一些常见的应用场景:
计数器
在高并发环境中,使用 sync/atomic
可以实现线程安全的计数器。例如,统计请求数、任务完成数等。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var counter int32
atomic.AddInt32(&counter, 1)
fmt.Println(counter) // 输出:1
}
标志位
使用原子操作可以实现线程安全的标志位,用于控制程序的执行流程。例如,标记某个任务是否已经完成。
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var flag int32
atomic.StoreInt32(&flag, 1)
if atomic.LoadInt32(&flag) == 1 {
fmt.Println("Flag is set")
}
}
指针交换
在某些情况下,需要在多个 goroutine 之间安全地交换指针。sync/atomic
提供了原子指针操作,确保指针交换的原子性。
package main
import (
"fmt"
"sync/atomic"
"unsafe"
)
func main() {
var ptr unsafe.Pointer
data := 42
atomic.StorePointer(&ptr, unsafe.Pointer(&data))
newData := 100
oldPtr := atomic.SwapPointer(&ptr, unsafe.Pointer(&newData))
fmt.Println(*(*int)(oldPtr)) // 输出:42
}
共享变量的原子操作
在多线程环境中,直接对共享变量进行非原子操作可能导致数据竞争和竞态条件。使用 sync/atomic
可以确保这些操作的原子性,避免数据竞争。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int32
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
for j := 0; j < 1000; j++ {
atomic.AddInt32(&counter, 1)
}
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter) // 输出:100000
}
4.context
上下文(Context)是 Go 语言中非常有特色的一个特性, 在 Go 1.7 版本中正式引入新标准库 context。其主要的作用是在 goroutine 中进行上下文的传递,而在传递信息中又包含了 goroutine 的运行控制、上下文信息传递等功能。
实际上,Go 语言是基于 context 来实现和搭建各类 goroutine 控制,并和 select 语句联合,可以实现上下文的截止时间、信号控制、信息传递等跨 goroutine 的操作。
context 特性
我们通过调用标准库 context.WithTimeout
方法针对 parentCtx
变量设置了超时时间,并在随后调用 select-case
进行 context.Done
方法的监听,最后由于达到截止时间。因此逻辑上 select
走到了 context.Err
的 case
分支,最终输出 context deadline exceeded
。
package main
import (
"context"
"fmt"
"time"
)
func main(){
// 声明一个空的上下文作为根上下文
parentCtx := context:Background()
// 返回一个带有超时时间的子 Context 和 一个取消函数
ctx, cancel := context.WithTimeout(parentCtx, 1*time.Millisecond)
// 执行取消函数
defer cancel()
select {
// 超时中断
case <- time.After(1 * time.Second):
fmt.Println("overslept")
case <- ctx.Done():
fmt.Println(ctx.Err())
}
}
除了上述方法,标准库 context
还支持下面的方法:
// 创建有截止时间的新 context 和 取消函数
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// 创建一个带有超时时间
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
type Context
// 创建空的 context,作为根的父级 context
func Background() Context
// 创建空的 context,用于未确定时的声明使用
func TODO() Context
// 基于某个 context 创建并存储对应的上下文信息
func WithValue(parent Context, key, val interface{}) Context
context 流程
context
相关函数的返回值分别是 Context
和 CancelFunc
,这里简要分析它们的作用。
Context接口
type Context interface {
// 获取 context 截止时间
Deadline() (deadline time.Time, ok bool)
// 识别只读 channel 是否关闭
Done() <-chan struct{}
// 获取被 context 关闭的原因
Err() error
// 获取当前 context 所存储的上下文信息
Value(key interface{}) interface{}
}
Canceler接口
type canceler interface {
// 调用当前 context 的取消方法
cancel(removeFromParent bool, err error)
// 识别当前 channel 是否关闭
Done() <-chan struct{}
}
在标准库 context
的设计上,提供了四类 context 类型来实现上述接口,分别是 emptyCtx
、cancelCtx
、timerCtx
以及 valueCtx
。
context 应用场景
context
包在 Go 语言中有许多实际应用,特别是在处理并发任务和网络请求时。以下是几个具体的应用案例:
HTTP 请求处理
在处理 HTTP 请求时,可以使用 context
来管理请求的生命周期,设置超时和传递请求范围内的数据。
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fmt.Println("Request started")
select {
case <-time.After(2 * time.Second):
fmt.Fprintf(w, "Request completed")
case <-ctx.Done():
fmt.Println("Request cancelled:", ctx.Err())
http.Error(w, ctx.Err().Error(), http.StatusInternalServerError)
}
})
srv := &http.Server{
Addr: ":8080",
Handler: http.TimeoutHandler(http.DefaultServeMux, 1*time.Second, "Timeout!"),
}
fmt.Println("Server started")
srv.ListenAndServe()
}
在这个示例中,HTTP 服务器设置了 1 秒的超时时间,如果请求在 1 秒内没有完成,将返回超时错误。
数据库操作
在数据库操作中,可以使用 context
来设置查询的超时和取消操作。
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
fmt.Println("Error connecting to the database:", err)
return
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
fmt.Println("Query error:", err)
return
}
defer rows.Close()
for rows.Next() {
// 处理结果
}
}
在这个示例中,数据库查询设置了 2 秒的超时时间,如果查询在 2 秒内没有完成,将返回超时错误。
并发任务管理
在并发任务中,可以使用 context
来管理多个 goroutine 的生命周期,确保在任务完成或取消时正确清理资源。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d cancelled\n", id)
return
default:
fmt.Printf("Worker %d working\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, &wg, i)
}
time.Sleep(2 * time.Second)
cancel()
wg.Wait()
fmt.Println("All workers cancelled")
}
在这个示例中,启动了三个并发的 worker goroutine,并在 2 秒后取消它们。
传递请求范围内的数据
可以使用 context
在函数调用链中传递请求范围内的数据,例如在日志中传递 trace_id
。
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.WithValue(context.Background(), "trace_id", "123456")
processRequest(ctx)
}
func processRequest(ctx context.Context) {
traceID := ctx.Value("trace_id").(string)
fmt.Println("Trace ID:", traceID)
}
在这个示例中,通过 context.WithValue
将 trace_id
传递给下游函数。