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. Python 读取图片 转 base64 并生成 JSON

    Python 读取图片 转 base64 并生成 JSON import json import base64 img_path = r'D:\OpenSource\PaddlePaddle\Padd ...

  2. 将MyBatis Mapper xml 放到 jar 包外面

    在不改程序的情况下,修改 sql 时,需要将 Mapper 中的 XML 文件 放到外面 mybatis:    mapper-locations: classpath:mapper/*.xml #J ...

  3. python sorted排序小结

    转载至: https://blog.csdn.net/ray_up/article/details/42084863 在python中排序有两个专用函数,一个是sort,另一个sorted.其中sor ...

  4. NBA赛事直播超清画质背后:阿里云视频云「窄带高清2.0」技术深度解读

    在半月前结束的NBA总决赛中,百视TV作为全网唯一采用"主播陪你看NBA"模式的直播平台,以"陪看型"赛事解说来面对内容差异化竞争.与此同时,百视TV还运用了& ...

  5. WPF 实现窗体鼠标事件穿透

    一.窗体变透明,需要加三个属性: AllowsTransparency="True"Background="Transparent"WindowStyle=&q ...

  6. #2059:龟兔赛跑(动态规划dp)

    Problem Description 据说在很久很久以前,可怜的兔子经历了人生中最大的打击--赛跑输给乌龟后,心中郁闷,发誓要报仇雪恨,于是躲进了杭州下沙某农业园卧薪尝胆潜心修炼,终于练成了绝技,能 ...

  7. Spring 学习笔记(2)框架介绍

    本篇文章主要对 Spring 框架进行整体介绍,包括其核心功能模块与体系结构,让大家对该框架有个大体的认识. 1. 前景提要 如果你之前学过 Servlet 的话,那么一定会对 MVC 分层概念有所了 ...

  8. NSSCTF Round#13 web专项

    rank:3 flask?jwt? 简单的注册个账号,在/changePassword 下查看页面源代码发现密钥<!-- secretkey: th3f1askisfunny --> ,很 ...

  9. SpringBoot 动态多线程并发定时任务

    一.简介 实现定时任务有多种方式: Timer:jdk 中自带的一个定时调度类,可以简单的实现按某一频度进行任务执行.提供的功能比较单一,无法实现复杂的调度任务. ScheduledExecutorS ...

  10. java项目实战-mybatis-基本配置01-day22

    目录 0. mysql navicate链接分享 1. mvn坐标引入 2. mysql的核心配置文件 3. 返回值类型 别名 4. 将数据的配置提取配置文件 4. log4j修改日志输出 0. my ...