浅谈监视器、条件变量、互斥锁

1. 监视器

先来看看Stanford的pintos文档中对监视器(monitor)的定义:

A monitor is a higher-level form of synchronization than a semaphore or a lock. A monitor consists of data being synchronized, plus a lock, called the monitor lock, and one or more condition variables. Before it accesses the protected data, a thread first acquires the monitor lock. It is then said to be “in the monitor”. While in the monitor, the thread has control over all the protected data, which it may freely examine or modify. When access to the protected data is complete, it releases the monitor lock.

​简而言之,监视器就是一种同步结构,提供了可同步访问的数据、一个锁和一个或多个条件变量来使线程们可以进行同步。


​使用比较形象的说明,监视器就像一个包含一个特殊房间(对象实例)的建筑物,每次只能由一个线程占用。这个房间通常包含一些需要防止并发访问的数据。从一个线程进入这个房间到它离开的时间,它可以独占访问房间中的任何数据(持有锁)。进入监控的建筑被称为“进入监控监视器。”进入建筑内部特殊的房间叫做“获取监视器”。房间占领被称为“拥有监视器”,离开房间被称为“释放监视器。”离开并让出整个建筑被称为“退出监视器”。
在一个线程访问受保护的数据(进入特殊的房间)之前,它得首先在建筑物接收(entry-set)中排队。如果没有其他线程在等待(拥有监视器),线程获取锁并继续执行受保护的代码。当线程完成执行时,它释放锁并退出建筑物(退出监视器)。


​下面的图片会有益于帮助读者理解这种机制:

在这里插入图片描述
如果当一个线程到达并且另一个线程已经拥有监视器时,它必须在接收队列中等待(entry-set)。当当前所有者退出监视器时,新到达的线程必须与在入口集中等待的其他线程竞争。只有一个线程能赢得竞争并拥有锁。而这部分内容与接下来要解释的条件变量和互斥锁有关。


2. 条件变量与互斥锁

​条件变量允许监视器中的代码等待条件变为真。每个条件变量都与一个抽象条件相关联,例如“一些数据已进行处理”或“自用户上一次按键以来已超过10秒”。以下说明几个常用的条件变量函数。当监视器中的线程需要等待条件变为真时,它将在关联的条件变量上“等待”,该变量将释放锁定并等待该条件被发出信号(cond_wait)。另一方面,如果它导致这些条件之一变为真,则它“发出信号”唤醒一个线程(cond_signal),或者“广播”唤醒所有线程(cond_broadcast)

2.1 pthread_cond_wait

​在调用pthread_cond_wait前要先获取锁。pthread_cond_wait函数执行时先自动释放指定的锁(监视器锁定),然后等待条件变量的变化。等待其它该条件变量发出cond信号(其它线程中signal或broadcast),然后再次取得锁。注意:在函数调用返回之前,自动将指定的互斥量重新锁住。

2.2 pthread_cond_signal

​如果有任何线程正在条件变量上等待,则该函数用以唤醒其中一个线程。存在多个等待线程时,按入队顺序激活其中一个。调用此函数之前必须保持锁定,调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait的最后一步是要将指定的互斥量重新锁住(见2.1),如果pthread_cond_signal之后没有释放互斥锁,pthread_cond_wait仍然要阻塞。

2.3 pthread_cond_broadcast

​与cond_signal类似,不过是唤醒所有线程。 调用此函数之前也必须保持锁定。

2.4 总结工作原理

​当我们想进行同步操作时,我们会首先调用pthread_cond_wait(lock, cond),这个函数首先解锁,于是此时,其它线程可以访问修改数据。然后它等待条件变量cond发出信号(signal或broadcast)。记住,此时wait函数并没有返回,这个线程正处于睡眠态。

​现在,互斥锁已被解开,其它线程可以占用资源(这里我们用2号线程这个说法来打个比方)。在进入”特殊房间“并占有资源后它立刻获得锁。修改数据完成后,它调用signal(lock, cond)或是broadcast(lock, cond)函数,立刻释放这个锁,使所有等待条件变量cond的线程立即苏醒。这意味着第一个线程(仍处于 pthread_cond_wait() 调用中)现在将苏醒

​此时,看一下第一个线程发生了什么。您可能会认为在 2 号线程调用 pthread_cond_broadcast(或是signal)之后,1 号线程的 pthread_cond_wait() 会立即返回。然而不是那样。实际上,pthread_cond_wait() 将执行最后一个操作:重新获得锁。一旦 pthread_cond_wait() 锁定了互斥对象,那么它将返回并允许 1 号线程继续执行。那时,它可以马上检查列表,查看它所感兴趣的更改。

3. 监视器实例——生产者消费者模型

​监视器的典型示例是利用生产者消费者模型来处理一个缓冲区,一个或多个“生产者”线程在其中写入字符,而一个或多个“消费者”线程从中读取字符。为了实现这一点,我们需要除了监视锁之外的两个条件变量,我们将它们称为 not_full和not_empty:

char buf[BUF_SIZE]; /* Buffer. */ 
size_t n = 0; /* 0 <= n <= BUF SIZE: # of characters in buffer. */ 
size_t head = 0; /* buf index of next char to write (mod BUF SIZE). */ 
size_t tail = 0; /* buf index of next char to read (mod BUF SIZE). */ 
struct lock lock; /* Monitor lock. */ 
struct condition not_empty; /* Signaled when the buffer is not empty. */ 
struct condition not_full; /* Signaled when the buffer is not full. */
...initialize the locks and condition variables...
void put (char ch) {
    
     
    lock_acquire (&lock); 
    while (n == BUF_SIZE) /* Can’t add to buf as long as it’s full. */ 
        cond_wait (&not_full, &lock); 
    buf[head++ % BUF_SIZE] = ch; /* Add ch to buf. */ 
    n++; 
    cond_signal (&not_empty, &lock); /* buf can’t be empty anymore. */ 
    lock_release (&lock); 
}
char get (void) {
    
     
    char ch; 
    lock_acquire (&lock); 
    while (n == 0) /* Can’t read buf as long as it’s empty. */ 
        cond_wait (&not_empty, &lock); 
    ch = buf[tail++ % BUF_SIZE]; /* Get ch from buf. */ 
    n--; 
    cond_signal (&not_full, &lock); /* buf can’t be full anymore. */ 
    lock_release (&lock); 
} 

猜你喜欢

转载自blog.csdn.net/weixin_44765402/article/details/109024067