go中x/sync/semaphore解读
semaphore
semaphore的作用
信号量是在并发编程中比较常见的一种同步机制,它会保证持有的计数器在0到初始化的权重之间,每次获取资源时都会将信号量中的计数器减去对应的数值,在释放时重新加回来,当遇到计数器大于信号量大小时就会进入休眠等待其他进程释放信号。
go中的semaphore,提供sleep和wakeup原语,使其能够在其它同步原语中的竞争情况下使用。当一个goroutine需要休眠时,将其进行集中存放,当需要wakeup时,再将其取出,重新放入调度器中。
go中本身提供了semaphore的相关方法,不过只能在内部调用
// go/src/sync/runtime.go
func runtime_Semacquire(s *uint32)
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)
扩展包golang.org/x/sync/semaphore提供了一种带权重的信号量实现方式,我们可以按照不同的权重对资源的访问进行管理。
如何使用
通过信号量来限制并行的goroutine数量,达到最大的maxWorkers数量,Acquire将会阻塞,直到其中一个goroutine执行完成,释放出信号量。
// Example_workerPool演示如何使用信号量来限制
// 用于并行任务的goroutine。
func main() {
ctx := context.Background()
var (
maxWorkers = runtime.GOMAXPROCS(0)
sem = semaphore.NewWeighted(int64(maxWorkers))
out = make([]int, 32)
)
// Compute the output using up to maxWorkers goroutines at a time.
for i := range out {
// When maxWorkers goroutines are in flight, Acquire blocks until one of the
// workers finishes.
if err := sem.Acquire(ctx, 1); err != nil {
log.Printf("Failed to acquire semaphore: %v", err)
break
}
go func(i int) {
defer sem.Release(1)
// doSomething
out[i] = i + 1
}(i)
}
// Acquire all of the tokens to wait for any remaining workers to finish.
//
// If you are already waiting for the workers by some other means (such as an
// errgroup.Group), you can omit this final Acquire call.
if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
log.Printf("Failed to acquire semaphore: %v", err)
}
fmt.Println(out)
}
分析下原理
type waiter struct {
// 信号量的权重
n int64
// 获得信号量后关闭
ready chan<- struct{}
}
// NewWeighted使用给定的值创建一个新的加权信号量
// 并发访问的最大组合权重。
func NewWeighted(n int64) *Weighted {
w := &Weighted{size: n}
return w
}
// 加权提供了一种方法来绑定对资源的并发访问。
// 呼叫者可以请求以给定的权重进行访问。
type Weighted struct {
// 表示最大资源数量,取走时会减少,释放时会增加
size int64
// 计数器,记录当前已使用资源数,值范围[0 - size]
cur int64
mu sync.Mutex
// 等待队列,表示申请资源时由于可使用资源不够而陷入阻塞等待的调用者列表
waiters list.List
Acquire
阻塞的获取指定权种的资源,如果没有空闲的资源,会进去休眠等待。
// Acquire获取权重为n的信号量,阻塞直到资源可用或ctx完成。
// 成功时,返回nil。失败时返回 ctx.Err()并保持信号量不变。
// 如果ctx已经完成,则Acquire仍然可以成功执行而不会阻塞
func (s *Weighted) Acquire(ctx context.Context, n int64) error {
s.mu.Lock()
// 如果资源足够,并且没有排队等待的waiters
// cur+n,直接返回
if s.size-s.cur >= n && s.waiters.Len() == 0 {
s.cur += n
s.mu.Unlock()
return nil
}
// 资源不够,err返回
if n > s.size {
// 不要其他的Acquire,阻塞在此
s.mu.Unlock()
<-ctx.Done()
return ctx.Err()
}
ready := make(chan struct{})
// 组装waiter
w := waiter{n: n, ready: ready}
// 插入waiters中
elem := s.waiters.PushBack(w)
s.mu.Unlock()
// 阻塞等待,直到资源可用或ctx完成
select {
case <-ctx.Done():
err := ctx.Err()
s.mu.Lock()
select {
case <-ready:
// 在canceled之后获取了信号量,不要试图去修复队列,假装没看到取消
err = nil
default:
s.waiters.Remove(elem)
}
s.mu.Unlock()
return err
// 等待者被唤醒了
case <-ready:
return nil
}
}
梳理下流程:
1、如果资源够用并且没有等待队列,添加已经使用的资源数;
2、如果超过资源数,抛出err;
3、资源够用,并且等待队列,将之后的加入到等待队列中;
4、阻塞直到资源可用或ctx完成。
TryAcquire
非阻塞地获取指定权重的资源,如果当前没有空闲资源,会直接返回false。
// TryAcquire获取权重为n的信号量而不阻塞。
// 成功时返回true。 失败时,返回false并保持信号量不变。
func (s *Weighted) TryAcquire(n int64) bool {
s.mu.Lock()
success := s.size-s.cur >= n && s.waiters.Len() == 0
if success {
s.cur += n
}
s.mu.Unlock()
return success
}
TryAcquire获取权重为n的信号量而不阻塞,相比Acquire少了等待队列的处理。
Release
用于释放指定权重的资源,如果有waiters则尝试去一一唤醒waiter。
// Release释放权值为n的信号量。
func (s *Weighted) Release(n int64) {
s.mu.Lock()
s.cur -= n
// cur的范围在[0 - size]
if s.cur < 0 {
s.mu.Unlock()
panic("semaphore: bad release")
}
s.notifyWaiters()
s.mu.Unlock()
}
func (s *Weighted) notifyWaiters() {
// 如果有阻塞的waiters,尝试去进行一一唤醒
// 唤醒的时候,先进先出,避免被资源比较大的waiter被饿死
for {
next := s.waiters.Front()
// 已经没有waiter了
if next == nil {
break
}
w := next.Value.(waiter)
// waiter需要的资源不足
if s.size-s.cur < w.n {
// 没有足够的令牌供下一个waiter使用。我们可以继续(尝试
// 查找请求较小的waiter),但在负载下可能会导致
// 饥饿的大型请求;相反,我们留下所有剩余的waiter阻塞
//
// 考虑一个用作读写锁的信号量,带有N个令牌,N个reader和一位writer
// 每个reader都可以通过Acquire(1)获取读锁。
// writer写入可以通过Acquire(N)获得写锁定,但不包括所有的reader。
// 如果我们允许读者在队列中前进,writer将会饿死-总是有一个令牌可供每个读者。
break
}
s.cur += w.n
s.waiters.Remove(next)
close(w.ready)
}
}
对于waiters的唤醒,遵循的原则总是先进先出。当有10个资源可以被使用,第一个waiter需要100个资源,第二个waiter需要1个资源。不会让第二个先释放,必须等待第一个waiter被释放。这样避免需要资源比较大waiter的被饿死,因为这样需要资源数比较小的waiter,总是可以被最先释放,需要资源比较大的waiter,就没有获取资源的机会了。
总结
Acquire和TryAcquire都可用于获取资源,Acquire是可以阻塞的获取资源,TryAcquire只能非阻塞的获取资源;
Release对于waiters的唤醒原则,总是先进先出,避免资源需求比较大的waiter被饿死;
参考
【Golang并发同步原语之-信号量Semaphor】https://blog.haohtml.com/archives/25563
【Go并发编程实战--信号量的使用方法和其实现原理】https://juejin.cn/post/6906677772479889422
本文作者:liz
本文链接:https://boilingfrog.github.io/2021/04/01/x-sync.semaphore/
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
go中x/sync/semaphore解读的更多相关文章
- BiLSTM-CRF模型中CRF层的解读
转自: https://createmomo.github.io/ BiLSTM-CRF模型中CRF层的解读: 文章链接: 标题:CRF Layer on the Top of BiLSTM - 1 ...
- vue 中的.sync语法糖
提到父子组件相互通信,可能大家的第一反应是$emit,最近在学着封装组件,以前都是用的别人封装好的UI组件,对vue中的.sync这个修饰符有很大的忽略,后来发现这个修饰符很nice,官方对她的描述是 ...
- golang中的sync
1. Go语言中可以使用sync.WaitGroup来实现并发任务的同步 package main import ( "fmt" "sync" ) func h ...
- go中waitGroup源码解读
waitGroup源码刨铣 前言 WaitGroup实现 noCopy state1 Add Wait 总结 参考 waitGroup源码刨铣 前言 学习下waitGroup的实现 本文是在go ve ...
- [JavaWeb]关于DBUtils中QueryRunner的一些解读.
前言:[本文属于原创分享文章, 转载请注明出处, 谢谢.]前面已经有文章说了DBUtils的一些特性, 这里再来详细说下QueryRunner的一些内部实现, 写的有错误的地方还恳请大家指出. Que ...
- java中的信号量Semaphore
Semaphore(信号量)充当了操作系统概念下的“信号量”.它提供了“临界区中可用资源信号量”的相同功能.以一个停车场运作为例.为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的.这时如果 ...
- week3编程作业: Logistic Regression中一些难点的解读
%% ============ Part : Compute Cost and Gradient ============ % In this part of the exercise, you wi ...
- Http协议中关于Content-Length的解读【出现坑爹的视频中断】
最近公司的视频设备在下载视频的时候,出现了很诡异的现象,在新旧服务器一样的tpp包,下载下来后,新服务器无法解析,旧服务器没问题.且tpp包并没有改动. 后面找了挺久,终于发现了视频下载的时候是断点续 ...
- [JavaWeb]关于DBUtils中QueryRunner的一些解读(转)
QueryRunner类 QueryRunner中提供对sql语句操作的API它主要有三个方法 query() 用于执行select update() 用于执行insert/update/delete ...
- golang中并发sync和channel
golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...
随机推荐
- JAVA性能优化- IntelliJ插件:java内存分析工具(JProfiler)
JProfiler(Java性能分析神器) v11.1.4 下载 安装目录不要有空格 安装成功后,在 Intellij 里面选择对应的 jprofiler.exe 路径 点击下图JProfiler图标 ...
- 初探: 通过pyo3用rust为python写扩展加速
众所周知,python性能比较差,尤其在计算密集型的任务当中,所以机器学习领域的算法开发,大多是将python做胶水来用,他们会在项目中写大量的C/C++代码然后编译为so动态文件供python加载使 ...
- Ansible--批量创建lvm
--- - hosts: all tasks: - block: - name: 创建1000M的逻辑卷lv1 lvol: vg: vg0 lv: lv1 size: 1000 - name: 逻辑卷 ...
- 【驱动】以太网扫盲(三)PHY的控制器驱动框架分析
1. 概述 PHY芯片为OSI的最底层-物理层(Physical Layer),通过MII/GMII/RMII/SGMII/XGMII等多种媒体独立接口(介质无关接口)与数据链路层的MAC芯片相连,并 ...
- WebGPU光追引擎基础课:使用WebGPU绘制三角形
大家好~我开设了"WebGPU光追引擎基础课"的线上课程,从0开始,在课上带领大家现场写代码,使用WebGPU开发基础的光线追踪引擎 课程重点在于基于GPU并行计算,实现BVH构建 ...
- vue 使用print.js实现前端打印功能
https://blog.csdn.net/cccdf_jjj/article/details/99563682 插件vue-print-nb实现前端打印当前页面功能 https://blog.csd ...
- 2023是AI爆发的元年,程序员赚钱的机会来了,附49个机会!
以下是程序员利用AI做代码生成的赚钱思路.方案,共49条,按照不同分类列出: 基于自然语言生成的机会: 1. 开发基于AI的自动生成代码软件,应用于网站开发.移动应用开发.家庭自动化.人工智能等各个领 ...
- spring启动流程 (3) BeanDefinition详解
BeanDefinition在Spring初始化阶段保存Bean的元数据信息,包括Class名称.Scope.构造方法参数.属性值等信息,本文将介绍一下BeanDefinition接口.重要的实现类, ...
- SV Interface and Program 2
Clocking:激励的时序 memory检测start信号,当start上升沿的时候,如果write信号拉高之后,将data存储到mem中 start\write\addr\data - 四个信号是 ...
- 15-Verilog Coding Style
Verilog Coding Style 1.为什么需要Coding Style 可综合性 - 代码需要综合成网表,如果写了一些不可综合的代码,会出现错误 可读性,代码通常有多个版本,所以需要保证代码 ...