Go并发编程-Semaphore
转自 :Go并发编程-Semaphore
信号量就是用一个变量来控制并发能力。在高并发的情况下,资源是有限的,多个线程对于资源的申请和使用,当资源不足时需要阻塞等待,当有资源释放出来时需要通知阻塞等待的线程获取资源继续处理。
P/V操作
在信号量中有两个操作。P操作(descrease, wait, acquire)是减小信号量的值。V操作(increase, signal, release)增加信号量的计数值。信号量更像一个资源池,p是从池子中获取资源,v就是将资源返还给池子。当池子中的资源用光时,新来的线程进入到等待队列,为了保证公平性,让队列去实现先进先出,以解决饥饿问题。
信号量
信号量可以分为计数信号量和二进制信号量。二进制信号量是一种特殊的计数信号量,计数值只能是0或者1。二进制信号量更像是一个互斥锁。
Go官方扩展实现
Go内部也是使用信号量来控制goroutine的阻塞和唤醒。在Mutex互斥锁实现的就是使用的信号量。第二个属性,他是使用一个二进制信号量来做的。
type Mutex struct {
state int32
sema uint32
}
信号量的 P/V 操作是通过函数实现的
func runtime_Semacquire(s *uint32)
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
func runtime_SemaRelease(s *uint32, handoff bool, skipframes int)
这个是Go内部运行时调用的,并没有暴露出来信号量的并发原语。但是Go在扩展包里面提供了信号量Weighted。在初始化的时候可以设置权重(初始化的资源数)。
func NewWeighted(n int64) *Weighted //初始化一个信号量
func (s *Weighted) Acquire(ctx context.Context, n int64) error //相当于P操作,可以使用context进行超时和取消
func (s *Weighted) TryAcquire(n int64) bool // 尝试获取资源
func (s *Weighted) Release(n int64) //相当于V操作
使用信号量实现一个使用与cpu核数一样多的worker,去处理一个slice
var (
maxWorkers = runtime.GOMAXPROCS(0)
sema = semaphore.NewWeighted(int64(maxWorkers))
task = make([]int, maxWorkers*4)
)
func TestWeighted(t *testing.T) {
ctx := context.Background()
for i := range task {
if err := sema.Acquire(ctx, 1); err != nil {
break
}
go func(i int) {
defer sema.Release(1)
task[i] = i + 1
}(i)
}
//通过获取全部数量的信号量,来保证所有的worker全部处理完
if err := sema.Acquire(ctx, int64(maxWorkers)); err != nil {
t.Error("获取所有的worker失败")
}
t.Log(task)
}
看实现
type Weighted struct {
size int64 //资源全部数量
cur int64 //使用资源的数量
mu sync.Mutex // 互斥锁
waiters list.List // 等待队列
}
P操作
func (s *Weighted) Acquire(ctx context.Context, n int64) error {
s.mu.Lock()
if s.size-s.cur >= n && s.waiters.Len() == 0 { // 当资源足够时,不需要考虑context.Done状态,直接给cur加上使用的数量
s.cur += n
s.mu.Unlock()
return nil
}
if n > s.size { // 申请的资源数量超过了本身最大值,是不可能完成的任务,依赖ctx的状态返回,否则一直阻塞。有返回时返回错误。
// Don't make other Acquire calls block on one that's doomed to fail.
s.mu.Unlock()
<-ctx.Done()
return ctx.Err()
}
ready := make(chan struct{})//资源不足时,查u你更加爱你一个chan方便通知。并且增加一个等待着加入到等待队列中
w := waiter{n: n, ready: ready}
elem := s.waiters.PushBack(w)
s.mu.Unlock()
select {
case <-ctx.Done():
err := ctx.Err()
s.mu.Lock()
select {
case <-ready:
err = nil
default:
isFront := s.waiters.Front() == elem
s.waiters.Remove(elem)
if isFront && s.size > s.cur {
s.notifyWaiters()
}
}
s.mu.Unlock()
return err
case <-ready:
return nil
}
}
V操作
func (s *Weighted) Release(n int64) {
s.mu.Lock()
s.cur -= n //使用数量减少
if s.cur < 0 {
s.mu.Unlock()
panic("semaphore: released more than held")
}
s.notifyWaiters() //通知其他等待着
s.mu.Unlock()
}
通知
func (s *Weighted) notifyWaiters() {
for {
next := s.waiters.Front()
if next == nil {
break // No more waiters blocked.
}
w := next.Value.(waiter)
if s.size-s.cur < w.n {
//避免饥饿,这里还是按照先入先出的方式处理
break
}
s.cur += w.n
s.waiters.Remove(next)
close(w.ready)
}
}
使用信号量常见的错误
- 请求了资源没有释放
- 释放了从未请求的资源
- 长时间持有一个资源,即使不使用它
- 不持有一个资源,直接使用它
扩展
对于信号量的实现,也是可以使用channel来实现,但是channel只能实现一个二进制信号量。在选择信号量时,如果是一次性获取多个资源时可以选择官方扩展的信号量。