goroutine
goroutine是golang中的并发执行单元,是一种轻量级的线程(或称为协程),用于并发执行函数或方法。每个goroutine都有自己的调用栈,并独立地运行于其他goroutine之间,由Go运行时系统进行管理和调度。
创建一个新的goroutine非常简单,只需要在函数或方法前面加上关键字"go"即可。
通过使用goroutine,我们可以在程序中轻松地实现并发处理任务,提高程序的并发性和响应能力。
chan
Do not communicate by sharing memory; instead, share memory by communicating.
"不要以共享内存的方式来通信,相反,要通过通信来共享内存。"
基于CSP并发模型golang设计了channel这种数据结构来实现goroutine之间的数据传输和同步。
go
rw := make(chan int) // 可读可写channel
var r <-chan int // 只读channel,只能对该channel进行读操作
var w chan<- int // 只写channel,只能对该channel进行写操作
go func(ch chan int /* 引用传递 */) {
unbuf := make(chan int) // 创建无缓冲channel
buf := make(chan int, 0) // 创建有缓冲channel,缓冲大小为 0,和无缓冲channel等价
for i:= range buf {
// 循环迭代读取缓冲chan中的数据
}
i := <- ch // 从channel读数据
ch <- i // 写数据到channel中
close(ch) // 关闭channel
i, ok := <-ch // 从channel读数据
}(rw)
使用注意事项:
- chan需要采用make进行创建和初始化
- 对只读chan进行写操作、对只写chan进行读操作会导致编译错误
- chan是一个引用类型,作为函数入参或返回值传递时进行浅拷贝
- chan中数据的读写是进行值复制,因此有性能损耗
channel存在3种状态
:
- nil,未初始化的状态,只进行了声明,或者手动赋值为
nil
- active,正常的channel,可读或者可写
- closed,已关闭,千万不要误认为关闭channel后,channel的值是nil
channel可进行3种操作
:
- 读
- 写
- 关闭
把这3种操作和3种channel状态可以组合出9种情况
:
操作 | nil的channel | 正常channel | 已关闭channel |
---|---|---|---|
<- ch (读) | 阻塞 | 成功或阻塞 | 读到零值 |
ch <- (写) | 阻塞 | 成功或阻塞 | panic |
close(ch) (关闭) | panic | 成功 | panic |
对于nil通道的情况,也并非完全遵循上表,有1个特殊场景 :当nil
的通道在select
的某个case
中时,这个case会阻塞,但不会造成死锁。
select-case-default
select
语句用于在多个case通道操作中进行选择。它可以同时等待多个通道的操作,并执行第一个可用的操作,如果有多个case通道操作可用,则会随机选择一个执行
go
func (ctx context.Context, ch chan int) {
select {
case <- ch: // 通道可用
// ...
case <- time.After(time.Second): // 操作超时处理
// ...
case ctx.Done(): // 上下文完成结束通知
// ...
default: // 未命中任何case
// ...
}
}
使用注意事项:
- 没有任何 case 的
select
语句会被编译器转换为runtime.block()
函数,永久阻塞 - 只有一个 channel 操作,实际会被编译器转换为相应channel 相应的收发操作,其实和实际调用
data := <- ch
并没有什么区别 - select 里面即可以对 channel 进行读取,还可以对 channel 进行写入,如果所有条件都不满足,并且有 default 子句,则执行 default 子句,否则,会阻塞
context.Context
包context 定义了 Context 类型,它在 API 边界和进程之间传递截止时间、取消信号和其他请求作用域值。
服务端的传入请求应该创建一个 Context,对服务端的外部调用应该接受一个 Context。它们之间的函数调用链必须传播 Context,并且可以使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建派生的 Context 来替换它。当一个 Context 被取消时,所有从它派生的 Context 也会被取消。
WithCancel、WithDeadline 和 WithTimeout 函数接受一个 Context(父级)并返回一个派生的 Context(子级)和一个 CancelFunc。调用 CancelFunc 会取消子级及其子级,移除父级对子级的引用并停止任何相关的定时器。如果不调用 CancelFunc,则会泄漏子级及其子级,直到父级被取消或定时器触发。go vet 工具检查所有控制流路径上是否使用了 CancelFuncs。
- 不要在结构类型中存储 Context;相反,将 Context 显式传递给需要它的每个函数(通常作为第一个参数,以ctx命名)
- 即使函数允许,也不要传递 nil Context。如果不确定要使用哪个 Context,请传递 context.TODO
- 仅将 context Value 用于跨进程和 API 传递的请求作用域数据,而不是将可选参数传递给函数
- 同一个 Context 可以传递给在不同 goroutine 中运行的函数;Context 可以同时被多个 goroutine 安全使用
go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error Value(key interface{}) interface{}
}
创建 context
context
包主要提供了两种方式创建context
:
context.Backgroud()
context.TODO()
这两个函数其实只是互为别名,没有差别,官方给的定义是:
context.Background
是上下文的默认值,通常作为根Context,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO
应该只在不确定应该使用哪种上下文时使用;- 在大多数情况下,我们都使用
context.Background
作为起始的上下文向下传递。
派生 context
scss
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
- WithCancel函数返回parent的副本并创建一个新的Done通道。当调用返回的cancel函数或父级上下文的Done通道关闭时,返回的上下文的Done通道关闭。
- WithDeadline函数返回父Context的副本,其截止时间早于或等于d。如果父Context的截止时间早于d,则WithDeadline(parent, d)在语义上等同于parent。返回的Context的Done通道在到期时关闭,当调用返回的cancel函数时关闭,或者当父Context的Done通道关闭时关闭,以先发生的事件为准。
- WithTimeout函数返回WithDeadline(parent, time.Now().Add(timeout))
- WithValue函数返回parent的副本,其中与键关联的值为val。仅将context Values用于跨进程和API传递的请求范围数据,而不是将可选参数传递给函数。所提供的key 必须是可比较的,不应该是字符串或任何其他内置类型。
总结
context.Context主要是用来给父协goroutine来控制子goroutine、孙goroutine、...的。当父goroutine调用了其创建的context.Context的cancel函数,该Context所派生的所有Context的cancel函数都将被调用,所有的Done通道都将写入完成。通过这种机制实现父子关系的goroutine的协同,防止父goroutine退出,子goroutine未退出,出现协程泄露。
另外就是作为跨goroutine调用的执行上下文,可以在跨goroutine调用时传递信息。
sync包中的工具
sync.Mutex 与 sync.RWMutex
Mutex(互斥锁)
- Mutex 为互斥锁,Lock() 加锁,Unlock() 解锁
- 在一个 goroutine 获得 Mutex 后,其他 goroutine 只能等到这个 goroutine 释放该 Mutex
- 使用 Lock() 加锁后,不能再继续对其加锁,直到利用 Unlock() 解锁后才能再加锁
- 在 Lock() 之前使用 Unlock() 会导致 panic 异常
- 已经锁定的 Mutex 并不与特定的 goroutine 相关联,这样可以利用一个 goroutine 对其加锁,再利用其他 goroutine 对其解锁
- 在同一个 goroutine 中的 Mutex 解锁之前再次进行加锁,会导致死锁
- 适用于读写不确定,并且只有一个读或者写的场景
RWMutex(读写锁)
- RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁
- 读锁占用的情况下会阻止写,不会阻止读,多个 goroutine 可以同时获取读锁
- 写锁会阻止其他 goroutine(无论读和写)进来,整个锁由该 goroutine 独占
- 适用于读多写少的场景
sync.WaitGroup
sync.WaitGroup主要用来使主goroutine等待并确知执行任务的子goroutine都已经完成了。
go
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
子goroutine在执行任务前先调用Add(1),执行完成后调用Done(),主goroutine则调用Wait()等待所有的子任务都执行完毕。
Add
方法用于将计数器增加delta
,可以为正数或负数。当计数器大于零时,Wait()
方法会阻塞。一般情况下,我们会在启动新的 Goroutine 前调用Add
方法,将需要等待的 Goroutine 数量添加到计数器中。Done
方法会将计数器减一,相当于通知WaitGroup
一个已完成的 Goroutine。一般情况下,我们会在 Goroutine 完成后调用Done
方法。Wait
方法会阻塞当前 Goroutine,直到计数器减为零,也就是所有被等待的 Goroutine 都已经完成执行。
sync.Once
sync.Once 是 Go 语言标准库中的一种同步原语,用于实现只执行一次的操作。
sync.Once 提供了一个 Do 方法,该方法接收一个函数作为参数,并保证该函数只会被执行一次,无论调用 Do 方法的次数有多少。在第一次调用 Do 方法时,传入的函数会被执行,而后续所有调用 Do 方法的操作都会被忽略。
sync.Cond
go
func NewCond(l Locker) *Cond
其中,NewCond 是一个函数,用于创建并初始化一个新的 sync.Cond 对象。它接收一个实现了 Locker 接口(如 sync.Mutex 或 sync.RWMutex)的互斥锁作为参数,并返回一个指向新创建的 sync.Cond 对象的指针。
go
func (c *Cond) Wait()
Wait 是 sync.Cond 类型的方法,用于在条件变量上等待通知。调用 Wait 方法会使当前 Goroutine 进入阻塞状态,直到收到其他 Goroutine 发送的通知信号。
go
func (c *Cond) Signal()
Signal 是 sync.Cond 类型的方法,用于发送单个通知信号。调用 Signal 方法会唤醒正在等待中的至少一个 Goroutine,使其从 Wait 方法的阻塞状态返回继续执行。
go
func (c *Cond) Broadcast()
Broadcast 是 sync.Cond 类型的方法,用于广播通知信号。调用 Broadcast 方法会唤醒所有等待中的 Goroutine,使它们从 Wait 方法的阻塞状态返回继续执行。
sync.Map 与 sync.Pool
sync.Map是原生map的并发安全版本
timer定时器
go
import "time"
// 创建一个 Timer 对象,并设置定时器的持续时间为 duration。
// 当时间到达 duration 后,定时器将触发。
timer := time.NewTimer(duration)
// 通过从 timer.C 的通道中接收一个值,程序将阻塞在此处直到定时器触发或者定时器被停止
<-timer.C
// 调用 timer.Stop() 可以停止定时器。
// 如果定时器还没有触发,调用 Stop 方法将会使定时器无效,防止触发事件的发生。
// 已经触发的定时器无法再被停止。
timer.Stop()