← 中文资源

简体中文版本

信号量

信号量是一个计数器,通过获取与释放许可限制可同时使用资源的线程或任务数量。

阅读英文原文

概念定义

信号量持有一个非负的许可计数。获取会使其递减,若将变为负数则阻塞;释放会使其递增并唤醒等待者。二元信号量等同于一把锁,而计数信号量把并发上限设为 n。与互斥锁不同,信号量没有归属——任何线程都可释放许可——这让它既适合用于生产者与消费者之间的信号,也适合用于限定资源。

为什么重要

无界并发正是服务耗尽连接、内存或下游配额的方式。信号量在使用点把隐式限制变为显式限制,让突发流量在边界处排队而不是级联成故障。在成千上万任务共享少量资源的异步运行时中,它是让并发量与容量相匹配的基本原语。

工程实践

把许可数设为受保护资源的真实容量,绝不能超过它。始终在 finally 式代码块中释放,使错误路径不会泄漏许可,并使用超时或有界等待,让调用方在资源饱和时不会永久挂起。当生产者与消费者需要会合时,一对信号量能干净地表达交接,但除非实现保证,否则不要假设等待者按序被服务。

代码示例

export class Semaphore {
  constructor(max) {
    if (!Number.isInteger(max) || max < 1) throw new RangeError("max must be a positive integer");
    this.max = max;
    this.active = 0;
    this.waiters = [];
  }
  get inFlight() {
    return this.active;
  }
  async acquire() {
    if (this.active < this.max) {
      this.active += 1;
      return this.makeRelease();
    }
    await new Promise((resolve) => this.waiters.push(resolve));
    return this.makeRelease();
  }
  makeRelease() {
    let released = false;
    return () => {
      if (released) return;
      released = true;
      this.active -= 1;
      const next = this.waiters.shift();
      if (next) {
        this.active += 1;
        next();
      }
    };
  }
  async run(task) {
    const release = await this.acquire();
    try {
      return await task();
    } finally {
      release();
    }
  }
  async runAll(tasks) {
    return Promise.all(tasks.map((t) => this.run(t)));
  }
}
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

type WeightedSemaphore struct {
	mu       sync.Mutex
	cond     *sync.Cond
	capacity int64
	used     int64
}

func NewWeightedSemaphore(capacity int64) (*WeightedSemaphore, error) {
	if capacity < 1 {
		return nil, fmt.Errorf("capacity must be positive")
	}
	sem := &WeightedSemaphore{capacity: capacity}
	sem.cond = sync.NewCond(&sem.mu)
	return sem, nil
}

func (s *WeightedSemaphore) Acquire(ctx context.Context, weight int64) error {
	if weight < 1 || weight > s.capacity {
		return fmt.Errorf("weight %d outside 1..%d", weight, s.capacity)
	}

	done := make(chan struct{})
	go func() {
		select {
		case <-ctx.Done():
			s.mu.Lock()
			s.cond.Broadcast()
			s.mu.Unlock()
		case <-done:
		}
	}()
	defer close(done)

	s.mu.Lock()
	defer s.mu.Unlock()
	for s.used+weight > s.capacity {
		if err := ctx.Err(); err != nil {
			return err
		}
		s.cond.Wait()
	}
	if err := ctx.Err(); err != nil {
		return err
	}
	s.used += weight
	return nil
}

func (s *WeightedSemaphore) Release(weight int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if weight < 1 || weight > s.used {
		panic(fmt.Sprintf("release %d with %d in use", weight, s.used))
	}
	s.used -= weight
	s.cond.Broadcast()
}

func main() {
	sem, err := NewWeightedSemaphore(3)
	if err != nil {
		fmt.Println(err)
		return
	}

	jobs := []struct {
		name   string
		weight int64
	}{
		{"small", 1},
		{"medium", 2},
		{"large", 3},
		{"small-2", 1},
	}

	var wg sync.WaitGroup
	for _, job := range jobs {
		job := job
		wg.Add(1)
		go func() {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			if err := sem.Acquire(ctx, job.weight); err != nil {
				fmt.Printf("%s: %v\n", job.name, err)
				return
			}
			time.Sleep(20 * time.Millisecond)
			sem.Release(job.weight)
			fmt.Printf("%s finished (weight %d)\n", job.name, job.weight)
		}()
	}
	wg.Wait()
	fmt.Println("all jobs done")
}