Go并发编程-Semaphore

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)
	}
}

使用信号量常见的错误

  1. 请求了资源没有释放
  2. 释放了从未请求的资源
  3. 长时间持有一个资源,即使不使用它
  4. 不持有一个资源,直接使用它

扩展

对于信号量的实现,也是可以使用channel来实现,但是channel只能实现一个二进制信号量。在选择信号量时,如果是一次性获取多个资源时可以选择官方扩展的信号量。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值