扩展并发原语
信号量(Semaphore/Weighted)
信号量(Semaphore/Weighted)是用来控制多个 goroutine 同时访问多个资源的并发原语
- 初始化信号量:设定初始的资源的数量。
- P 操作:将信号量的计数值减去 1,如果新值已经为负,那么调用者会被阻塞并加入到等待队列中。否则,调用者会继续执行,并且获得一个资源。
- V 操作:将信号量的计数值加 1,如果先前的计数值为负,就说明有等待的 P 操作的调用者。它会从等待队列中取出一个等待的调用者,唤醒它,让它继续执行。
一般用信号量保护一组资源,比如数据库连接池、一组客户端的连接、几个打印机资源,等等
- Acquire 方法:相当于 P 操作,你可以一次获取多个资源,如果没有足够多的资源,调用者就会被阻塞。它的第一个参数是 Context,这就意味着,你可以通过 Context 增加超时或者 cancel 的机制。如果是正常获取了资源,就返回 nil;否则,就返回 ctx.Err(),信号量不改变。
- Release 方法:相当于 V 操作,可以将 n 个资源释放,返还给信号量。
- TryAcquire 方法:尝试获取 n 个资源,但是它不会阻塞,要么成功获取 n 个资源,返回 true,要么一个也不获取,返回 false。
tips:如果在实际应用中,你想等所有的 Worker 都执行完,就可以获取最大计数值的信号量。
使用信号量的常见错误
- 请求了资源,但是忘记释放它;
- 释放了从未请求的资源;
- 长时间持有一个资源,即使不需要它;
- 不持有一个资源,却直接使用它。
SingleFlight(幂等性) 和 CyclicBarrier(循环栅栏/组装工厂)
SingleFlight :将并发请求合并成一个请求,以减少对下层服务的压力
CyclicBarrier :可重用的栅栏并发原语,用来控制一组请求同时执行的数据结构
请求合并 SingleFlight
作用:在处理多个 goroutine 同时调用同一个函数的时候,只让一个 goroutine 去调用这个函数,等到这个 goroutine 返回结果的时候,再把结果返回给这几个同时调用的 goroutine,这样可以减少并发调用的数量
应用:面对秒杀等大并发请求的场景,而且这些请求都是读请求时,可以把这些请求合并为一个请求
- Do:这个方法执行一个函数,并返回函数执行的结果。你需要提供一个 key,对于同一个 key,在同一时间只有一个在执行,同一个 key 并发的请求会等待。第一个执行的请求返回的结果,就是它的返回结果。函数 fn 是一个无参的函数,返回一个结果或者 error,而 Do 方法会返回函数执行的结果或者是 error,shared 会指示 v 是否返回给多个请求。
- DoChan:类似 Do 方法,只不过是返回一个 chan,等 fn 函数执行完,产生了结果以后,就能从这个 chan 中接收这个结果。
- Forget:告诉 Group 忘记这个 key。这样一来,之后这个 key 请求会执行 f,而不是等待前一个未完成的 fn 函数的结果。
缓存穿透:查询一个数据库里压根就没有的数据
- 结局办法:缓存空对象、布隆过滤器
缓存雪崩:大量不同的 Key 在同一时间集体失效,导致流量直击数据库
- 解决办法:高可用缓存集群、过期时间打散、服务降级与限流
缓存击穿:某一个热点 Key 过期,导致海量并发请求同时“重建”这个 Key 的缓存
- 解决办法:互斥锁、热点数据永不过期
groupcache
一致性哈希和 singleflight 思想
- 只适用于那些希望简化部署、并且主要缓存需求是防止热点数据击穿的特定场景
循环栅栏 CyclicBarrier
CyclicBarrier 更适合用在“固定数量的 goroutine 等待同一个执行点”的场景中,而且在放行 goroutine 之后,CyclicBarrier 可以重复利用
WaitGroup 更适合用在“一个 goroutine 等待一组 goroutine 到达同一个执行点”的场景中,或者是不需要重用的场景中
两个初始化方法:
- New 方法,它只需要一个参数,来指定循环栅栏参与者的数量
- NewWithAction,它额外提供一个函数,可以在每一次到达执行点的时候执行一次。具体的时间点是在最后一个参与者到达之后,但是其它的参与者还未被放行之前
并发趣题:一氧化二氢(水)制造工厂
分组操作
分组执行一批相同的或类似的任务
ErrGroup
将一个大的任务拆成几个小任务并发执行
- WithContext 方法:返回一个 Group 实例,同时还会返回一个使用 context.WithCancel(ctx) 生成的新 Context。一旦有一个子任务返回错误,或者是 Wait 调用返回,这个新 Context 就会被 cancel
- Go 方法:传入的子任务函数 f 是类型为 func() error 的函数,如果任务执行成功,就返回 nil,否则就返回 error,并且会 cancel 那个新的 Context
- Wait 方法:类似 WaitGroup
扩展库:bilibili/errgroup、neilotoole/errgroup、facebookgo/errgroup
gollback
- All 方法:等待所有的异步函数(AsyncFunc)都执行完才返回,而且返回结果的顺序和传入的函数的顺序保持一致。第一个返回参数是子任务的执行结果,第二个参数是子任务执行时的错误信息
- Race 方法:只要一个异步函数执行没有错误,就立马返回,而不会返回所有的子任务信息。如果所有的子任务都没有成功,就会返回最后一个 error 信息
- Retry 方法:执行一个子任务。如果子任务执行失败,它会尝试一定的次数,如果一直不成功 ,就会返回失败错误
Hunch
- All 方法:它会传入一组可执行的函数(子任务),返回子任务的执行结果。和 gollback 的 All 方法不一样的是,一旦一个子任务出现错误,它就会返回错误信息,执行结果(第一个返回参数)为 nil。
- Take 方法:可以指定 num 参数,只要有 num 个子任务正常执行完没有错误,这个方法就会返回这几个子任务的结果。一旦一个子任务出现错误,它就会返回错误信息,执行结果(第一个返回参数)为 nil。
- Last 方法:只返回最后 num 个正常执行的、没有错误的子任务的结果。一旦一个子任务出现错误,它就会返回错误信息,执行结果(第一个返回参数)为 nil
- Retry 方法:和gollback 的 Retry 方法的功能一样
- Waterfall 方法:子任务都是串行执行的,前一个子任务的执行结果会被当作参数传给下一个子任务
schedgroup
和时间相关的处理一组 goroutine 的并发原语
-
Delay 和 Schedule方法:都是用来指定在某个时间或者之后执行一个函数;Delay 处理相对时间,而 Schedule 处理绝对时间
-
Wait 方法:阻塞调用者,直到之前安排的所有子任务都执行完才返回
注意:
- 如果调用了 Wait 方法,你就不能再调用它的 Delay 和 Schedule 方法,否则会 panic
- Wait 方法只能调用一次,如果多次调用的话,就会 panic