脑抽研究生Go并发-3-拓展并发原语-信号量、SingleFlight(幂等性) 和 CyclicBarrier(循环栅栏)、分组操作(ErrGroup)等

59 阅读7分钟

扩展并发原语

信号量(Semaphore/Weighted)

img

信号量(Semaphore/Weighted)是用来控制多个 goroutine 同时访问多个资源的并发原语

  • 初始化信号量:设定初始的资源的数量。
  • P 操作:将信号量的计数值减去 1,如果新值已经为负,那么调用者会被阻塞并加入到等待队列中。否则,调用者会继续执行,并且获得一个资源。
  • V 操作:将信号量的计数值加 1,如果先前的计数值为负,就说明有等待的 P 操作的调用者。它会从等待队列中取出一个等待的调用者,唤醒它,让它继续执行。

一般用信号量保护一组资源,比如数据库连接池、一组客户端的连接、几个打印机资源,等等

  1. Acquire 方法:相当于 P 操作,你可以一次获取多个资源,如果没有足够多的资源,调用者就会被阻塞。它的第一个参数是 Context,这就意味着,你可以通过 Context 增加超时或者 cancel 的机制。如果是正常获取了资源,就返回 nil;否则,就返回 ctx.Err(),信号量不改变。
  2. Release 方法:相当于 V 操作,可以将 n 个资源释放,返还给信号量。
  3. TryAcquire 方法:尝试获取 n 个资源,但是它不会阻塞,要么成功获取 n 个资源,返回 true,要么一个也不获取,返回 false。

tips:如果在实际应用中,你想等所有的 Worker 都执行完,就可以获取最大计数值的信号量。

使用信号量的常见错误

  • 请求了资源,但是忘记释放它;
  • 释放了从未请求的资源;
  • 长时间持有一个资源,即使不需要它;
  • 不持有一个资源,却直接使用它。

SingleFlight(幂等性) 和 CyclicBarrier(循环栅栏/组装工厂)

img

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,它额外提供一个函数,可以在每一次到达执行点的时候执行一次。具体的时间点是在最后一个参与者到达之后,但是其它的参与者还未被放行之前

并发趣题:一氧化二氢(水)制造工厂

分组操作

img

分组执行一批相同的或类似的任务

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