semaphore

semaphore的作用

信号量是在并发编程中比较常见的一种同步机制,它会保证持有的计数器在0到初始化的权重之间,每次获取资源时都会将信号量中的计数器减去对应的数值,在释放时重新加回来,当遇到计数器大于信号量大小时就会进入休眠等待其他进程释放信号。

go中的semaphore,提供sleepwakeup原语,使其能够在其它同步原语中的竞争情况下使用。当一个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,就没有获取资源的机会了。

总结

AcquireTryAcquire都可用于获取资源,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解读的更多相关文章

  1. BiLSTM-CRF模型中CRF层的解读

    转自: https://createmomo.github.io/ BiLSTM-CRF模型中CRF层的解读: 文章链接: 标题:CRF Layer on the Top of BiLSTM - 1  ...

  2. vue 中的.sync语法糖

    提到父子组件相互通信,可能大家的第一反应是$emit,最近在学着封装组件,以前都是用的别人封装好的UI组件,对vue中的.sync这个修饰符有很大的忽略,后来发现这个修饰符很nice,官方对她的描述是 ...

  3. golang中的sync

    1. Go语言中可以使用sync.WaitGroup来实现并发任务的同步 package main import ( "fmt" "sync" ) func h ...

  4. go中waitGroup源码解读

    waitGroup源码刨铣 前言 WaitGroup实现 noCopy state1 Add Wait 总结 参考 waitGroup源码刨铣 前言 学习下waitGroup的实现 本文是在go ve ...

  5. [JavaWeb]关于DBUtils中QueryRunner的一些解读.

    前言:[本文属于原创分享文章, 转载请注明出处, 谢谢.]前面已经有文章说了DBUtils的一些特性, 这里再来详细说下QueryRunner的一些内部实现, 写的有错误的地方还恳请大家指出. Que ...

  6. java中的信号量Semaphore

    Semaphore(信号量)充当了操作系统概念下的“信号量”.它提供了“临界区中可用资源信号量”的相同功能.以一个停车场运作为例.为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的.这时如果 ...

  7. week3编程作业: Logistic Regression中一些难点的解读

    %% ============ Part : Compute Cost and Gradient ============ % In this part of the exercise, you wi ...

  8. Http协议中关于Content-Length的解读【出现坑爹的视频中断】

    最近公司的视频设备在下载视频的时候,出现了很诡异的现象,在新旧服务器一样的tpp包,下载下来后,新服务器无法解析,旧服务器没问题.且tpp包并没有改动. 后面找了挺久,终于发现了视频下载的时候是断点续 ...

  9. [JavaWeb]关于DBUtils中QueryRunner的一些解读(转)

    QueryRunner类 QueryRunner中提供对sql语句操作的API它主要有三个方法 query() 用于执行select update() 用于执行insert/update/delete ...

  10. golang中并发sync和channel

    golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...

随机推荐

  1. PPT 动画入门

    元素动画 进入动画 元素从无到有的过程 退出动画 元素从有到无的过程 退出动画和进入动画,一对一 强调动画 在元素上变化的过程(如放大) 动作路径 3D动画 三维动画 低版本不支持 组合动画 切换动画 ...

  2. Jenkins 多分支流水线(SVN)

    实际应用过程中,一般多分支流水线的方式用得比较多一些, master 对应 生成环境 develop 对应 测试环境, 将不同分支的代码构建到不同的环境中 添加 Jenkinsfile 文件 Jenk ...

  3. 比 Python 快得吓人,PyPy 极简入门

    众所周知 Python 有一个致命的缺点:速度比 C.C ++ 等语言慢很多.PyPy 恰好可以解决这一问题,它能够让 Python 代码运行得比 C 还快. 比如: import time from ...

  4. 【每日一题】32. 比赛 (DFS / 概率DP)

    补题链接:Here [方案一:DFS] 首先我们可以计算出每道题做不出来的概率 \(unsolve[i] = (1 - a[i])(1- b[i])(1 - c[i])\) 然后因为只有 12 道题, ...

  5. 强烈建议收藏,python库大全

    Python常用库大全及简要说明 本文为大家罗列了Python开发的常用库和各个库的简要说明以及Python开发工具,包管理,环境管理等其它常用资源和Python学习资料.本文只罗列了一部分,完整内容 ...

  6. 机器学习笔记(二)使用paddlepaddle,再探波士顿房价预测

    目标 用paddlepaddle来重写之前那个手写的梯度下降方案,简化内容 流程 实际上就做了几个事: 数据准备:将一个批次的数据先转换成nparray格式,再转换成Tensor格式 前向计算:将一个 ...

  7. gitlab安装,移库,升级

    概述 最近因为机房原因,需要把我们的本地代码库做移库操作. 针对gitlab的安装升级操作重新进行了梳理,记录一下. 环境 CENTOS6 CENTOS7 gitlab-ce-8.14.2 GITLA ...

  8. 接口自动化复习第四天利用正则和faker提取替换变量值

    在做接口自动化测试的时候,我们经常会遇到,有些字段利用随机生成数据就行了,不需要自己去构造测试数据.今天我就是要python中的第三方库faker来构造随机数,其次使用正则表达式来提取变量. 首先在接 ...

  9. 线段树 hdu 4027

    ***又是超时的问题,当一个区间全是1时,再去开方和不开方是一样的,所以在这一步不需要再往底层递归了*** #include <iostream> #include <cstdio& ...

  10. java基础-运算符--day03

    目录 1. 算数运算符 2.+号 3. ++ 4. = 赋值操作 5. 关系运算 6. 逻辑运算 7. 三元运算 8 位运算 1. 算数运算符 / 处以 % 取余 public class TestO ...