Concurrent Programming Series-Semaphore

Semaphore, usually translated as "semaphore" today, was also translated as "signal light" in the past, because similar to the traffic lights in real life, whether the vehicle can pass depends on whether the light is green. Similarly, in the programming world, whether a thread can execute depends on whether the semaphore is allowed.

The semaphore was proposed by the famous computer scientist Dijkstra in 1965. Until the tube was proposed in 1980, it has been the dominant method in the field of concurrent programming. Nowadays, almost all languages ​​that support concurrent programming support the semaphore mechanism, so mastering semaphores is still very necessary.

Below we first introduce the semaphore model, then introduce how to use semaphores, and finally use semaphores to implement a flow controller.

Semaphore model

The semaphore model is still very simple and can be simply summarized as: a counter, a waiting queue, and three methods . In the semaphore model, counters and waiting queues are transparent to the outside world, so they can only be accessed through the three methods provided by the semaphore model: init(), down(), and up(). You can understand it visually with the help of the picture below.

alt

The specific meanings of these three operations are as follows.

  • Initialization (init()): Set the starting value of the counter.
  • Reduce (down()): Decrease the counter value by 1; if the counter value is less than 0 at this time, the current thread will be blocked, otherwise the current thread can continue to execute.
  • Increase (up()): Add 1 to the counter value; if the counter value is less than or equal to 0 at this time, wake up a thread in the waiting queue and remove it from the waiting queue.

The three operations mentioned above, init(), down(), and up(), are all atomic, and this atomicity is guaranteed by the implementation of the semaphore model. In the Java SDK, the semaphore model is implemented by the java.util.concurrent.Semaphore class. The Semaphore class can ensure that these three operations are atomic operations.

If you feel that the above description is a bit complicated, you can refer to the following code to understand the implementation of the semaphore model.

class Semaphore{
    
    
  // 计数器
  int count;
  // 等待队列
  Queue queue;
  // 初始化操作
  Semaphore(int c){
    this.count=c;
  }
  //
  void down(){
    this.count--;
    if(this.count<0){
      //将当前线程插入等待队列
      //阻塞当前线程
    }
  }
  void up(){
    this.count++;
    if(this.count<=0) {
      //移除等待队列中的某个线程T
      //唤醒线程T
    }
  }
}

这里再插一句,信号量模型里面,down()、up()这两个操作历史上最早称为P操作和V操作,所以信号量模型也被称为PV原语。另外,还有些人喜欢用semWait()和semSignal()来称呼它们,虽然叫法不同,但是语义都是相同的。在Java SDK并发包里,down()和up()对应的则是acquire()和release()。

如何使用信号量

通过前文,你应该会发现信号量的模型还是非常简单的,那具体应该如何使用呢?其实你可以想象一下红绿灯。在十字路口,红绿灯可以控制交通流量,这得益于一个重要的规则:车辆在通过路口前必须检查是否为绿灯,只有绿灯才能通行。这个规则与我们之前提到的锁规则很相似,不是吗?

实际上,信号量的使用也是类似的。让我们继续用累加器的例子来说明信号量的使用吧。在累加器的例子中,count += 1操作是一个临界区,只允许一个线程执行,也就是说需要保证互斥性。那么,在这种情况下,如何使用信号量来控制呢?

实际上非常简单,就像我们使用互斥锁一样,只需要在进入临界区之前执行一次down()操作,在退出临界区之前执行一次up()操作就可以了。下面是Java代码的示例,acquire()就是信号量中的down()操作,release()就是信号量中的up()操作。

static int count;
//初始化信号量
static final Semaphore s
    = new Semaphore(1);
//用信号量保证互斥
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}

接下来我们再来分析一下,信号量是如何确保互斥性的。假设有两个线程T1和T2同时访问addOne()方法,当它们同时调用acquire()时,由于acquire()是一个原子操作,因此只能有一个线程(假设是T1)将信号量中的计数器减为0,而另一个线程(T2)将计数器减为-1。对于线程T1来说,信号量中计数器的值是0,大于等于0,所以线程T1会继续执行;对于线程T2来说,信号量中计数器的值是-1,小于0,根据信号量模型中对down()操作的描述,线程T2将被阻塞。因此,此时只有线程T1可以进入临界区并执行 count += 1;

当线程T1执行release()操作,也就是up()操作时,信号量中计数器的值是-1,经过加1后的值是0,小于等于0,根据信号量模型中对up()操作的描述,此时等待队列中的T2将被唤醒。于是,在T1执行完临界区代码之后,T2才有机会进入临界区执行,从而确保了互斥性。

快速实现一个限流器

上述的示例,我们通过使用信号量实现了一个简单的互斥锁功能。也许你会觉得奇怪,既然Java的SDK中已经提供了Lock,为什么还需要提供Semaphore呢?实际上,互斥锁只是Semaphore的一部分功能,而Semaphore还有一个Lock无法实现的功能,那就是:允许多个线程访问临界区

实际情况中确实存在这样的需求。比较常见的例子是我们在工作中遇到的各种资源池,比如连接池、对象池、线程池等等。其中,你可能对数据库连接池最为熟悉,在同一时刻,允许多个线程同时使用连接池,但每个连接在释放之前不允许其他线程使用。

实际上,不久前我在工作中也遇到了一个对象池的需求。对象池指的是一次性创建N个对象,然后所有线程重复利用这些对象,当然在对象释放之前不允许其他线程使用。对象池可以使用List来保存实例对象,这很简单。但关键在于限流器的设计,这里的限流指的是不允许超过N个线程同时进入临界区。那么如何快速实现这样的限流器呢?我立刻想到了使用信号量的解决方案。

在上面的例子中,信号量的计数器被设置为1,这个1表示只允许一个线程进入临界区。但是,如果我们将计数器的值设为对象池中对象的数量N,就可以完美解决对象池的限流问题了。下面是一个对象池的示例代码。

class ObjPool<T, R> {
    
    
  final List<T> pool;
  // 用信号量实现限流器
  final Semaphore sem;
  // 构造函数
  ObjPool(int size, T t){
    pool = new Vector<T>(){};
    for(int i=0; i<size; i++){
      pool.add(t);
    }
    sem = new Semaphore(size);
  }
  // 利用对象池的对象,调用func
  R exec(Function<T,R> func) {
    T t = null;
    sem.acquire();
    try {
      t = pool.remove(0);
      return func.apply(t);
    } finally {
      pool.add(t);
      sem.release();
    }
  }
}
// 创建对象池
ObjPool<Long, String> pool =
  new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
    System.out.println(t);
    return t.toString();
});

We use a List to store object instances and use Semaphore to implement the current limiter. The key code is located in the exec() method of ObjPool, which implements the current limiting function. In this method, we first call the acquire() method (the release() method will be called in the finally block). Assume that the object pool size is 10 and the semaphore counter is initialized to 10. Then the first 10 threads call the acquire() method. After that, execution can continue, which is equivalent to passing the semaphore, while other threads will be blocked on the acquire() method. After passing the semaphore thread, we allocate an object t to each thread (implemented through pool.remove(0)), and execute a callback function func. The parameter of the callback function is exactly the previously allocated object t; after executing the callback After the function, they release the object (implemented through pool.add(t)) and call the release() method to update the semaphore counter. If the counter value of the semaphore is less than or equal to 0 at this time, it means that a thread is waiting, and the waiting thread will be automatically awakened at this time.

In short, using semaphores, we can easily implement a current limiter and it is very simple to use.

Summarize

Semaphores are relatively unknown in the Java language, but are very famous in other programming languages. Java has made rapid development in the field of concurrent programming, focusing on supporting the monitor model. The tube process model theoretically solves some shortcomings of the semaphore model, mainly in terms of ease of use and engineering. For example, using semaphores to solve the blocking queue problem we mentioned earlier is much more complicated than using the monitor model. If you are interested, you can learn about it and try it.

This article is published by mdnice multi-platform

Guess you like

Origin blog.csdn.net/qq_35030548/article/details/132288841