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. Java SpringBoot Bean InitializingBean 项目初始化

    Spring中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean,即FactoryBean.工厂Bean跟普通Bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂Bean的ge ...

  2. Axure 多平台自适应

    步骤一:设置自适应视图 1.新建两个页面分别命名为"PC版"和"移动版" 2.启动自适应视图: 条件为大于等于,宽为1024,继承于基本视图3.新建自适应视图& ...

  3. ABAP 辨析 标准表|排序表|哈希表

    1.文档介绍 本文档将介绍内表的区别和用法,涉及标准表.排序表.哈希表 2.用法与区别 2.1.内表种类 内表顶层为任意表,任意表分为索引表和哈希表,索引表又可分为标准表和排序表,结构如图: 2.2. ...

  4. SuperSocket 简单示例

    这是一个SuperSocket 简单示例,包括服务端和客户端. 一.首先使用NuGet安装SuperSocket和SuperSocket.Engine 二.实现IRequestInfo(数据包): 数 ...

  5. docker centos 容器时间与宿主机时间不一致

    上图 容器时间不一致会造成N多问题,估计各位看官儿应该深有体会. 我处理的方式是在,dockerfile 中进行增加一条命令进行设置: RUN cp /usr/share/zoneinfo/Asia/ ...

  6. PySpark 报错 java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

    解决方案: mv mysql-connector-java-8.0.20.jar $SPARK_HOME/jars/ 驱动文件mysql-connector-java-8.0.20.jar是从mave ...

  7. PTA 天梯赛 L3-003 社交集群(并查集)

    当你在社交网络平台注册时,一般总是被要求填写你的个人兴趣爱好,以便找到具有相同兴趣爱好的潜在的朋友.一个"社交集群"是指部分兴趣爱好相同的人的集合.你需要找出所有的社交集群. 输入 ...

  8. 30例 | 一文搞懂python日期时间处理

    前言 datetime是python的内置模块,用来处理日期和时间. 该模块常用的类有: 类名 功能说明 date 日期对象 time 时间对象 datetime 日期时间对象 timedelta 时 ...

  9. 【收藏】制作艺术二维码,用 Stable Diffusion 就行!

    [收藏]Stable Diffusion 制作光影文字效果 https://www.cnblogs.com/Serverless/p/17620406.html 基于函数计算FC 快捷部署 Stabl ...

  10. 深度学习(三)——Transforms的使用

    一.Transforms的结构及用法 导入transforms from torchvision import transforms 作用:图片输入transforms后,可以得到一些预期的变换 1. ...