用于线程同步的Sleepon锁
在多线程程序中常遇到的另外一个情况就是让线程等待某件事的发生。这件事可以是任何事。它可以是设备上的数据就绪了,也可以是传送带到达了合适的位置或数据已经写入磁盘了,等等。另外还要讨论一下多个线程等待某个事件的情况。
为了实现这个功能,我们可以使用条件变量(condition variable)或是更简单的睡眠锁(sleepon lock)。
要使用睡眠锁,你需要执行几个操作。先看看要调用的函数,之后再看看你该如何使用这个锁:
int pthread_sleepon_lock (void); int pthread_sleepon_unlock (void); int pthread_sleepon_broadcast (void *addr); int pthread_sleepon_signal (void *addr); int pthread_sleepon_wait (void *addr);
在这里不要被这些函数的pthread前缀所误导,以为它们是POSIX函数,它们其实不是POSIX函数。
如上面所述,一个线程需要等待某件事发生。上面列出的函数中pthread_sleepon_wait()函数应该就是最直接的选择。但是,在你的程序中,这个线程先要检查它是否需要等待。先举例说明,一个线程是生产者线程,它从硬件读取数据,另外一个线程是消费者线程,它对新到来的数据做某种处理。下面先看看消费者线程:
volatile int data_ready = 0;
consumer ()
{
while (1) {
while (!data_ready) {
// WAIT
}
// process data
}
}
这个线程一直在其主处理循环(那个while(1)循环)中,它准备永远做这个工作。它做的第一件事就是查看那个data_ready标志量。如果这个量为0,就表示没有就绪的数据。这样的话,这个线程就进入等待状态。有些时候,生产者线程会唤醒它,之后这个消费者线程会重新检查data_ready这个标志量。假设这就是真实发生的,消费者线程检查标志量并确认为1,也就是数据已经就绪了。之后消费者线程开始处理数据,并检查是否有更多的工作可做,如此往复。
在这里我们会有一个问题。就是消费者线程与生产者线程在同步模式下是如何重置这个data_ready标志量的?显然,我们需要对这个标志量有某种独占式的访问以使其在任意时刻只有一个线程能修改它。在这里我们是使用互斥体为基础实现的,不过这个互斥体的实现是封装在sleepon的库函数中的,在这里我们只能使用两个函数:pthread_sleepon_lock()与pthread_sleepon_unlock()来访问它。下面是修改后的消费者线程的代码:
consumer ()
{
while (1) {
pthread_sleepon_lock ();
while (!data_ready) {
// WAIT
}
// process data
data_ready = 0;
pthread_sleepon_unlock ();
}
}
现在我们在消费者线程里面田间了锁定与解锁操作。这就意味着消费者线程可以可靠的检查data_ready标志量,而不会受到线程竞争的影响,同时也可以可靠的复位这个标志量。
不错吧!那么等待调用该如何处理呢?如我们上面建议的,使用pthread_sleepon_wait()函数。下面是我们的第二个while循环:
while (!data_ready) {
pthread_sleepon_wait (&data_ready);
}
这个pthread_sleepon_wait()函数实际上做了三步不同的操作:
- 解锁sleepon库函数的互斥体;
- 执行等待操作;
- 重新锁定sleepon库函数的互斥体。
它必须解锁与锁定sleepon库函数互斥体的理由很简单:由于使用互斥体的目的就是确保data_ready标志量的互斥性,也就是说我们在检查这个变量的值的时候就要将生产者线程锁定在外以防止其修改这个变量。不过,如果我们不做解锁的操作的话,生产者线程将永远也没有机会修改这个变量来告诉我们数据已经就绪了。而最后的重新锁定操作是纯粹为了方便起见,这样的话,pthread_sleepon_wait()的用户就没有必要在其唤醒的时候担心这个锁的状态。
下面转到生产者线程这里,看看它是如何使用sleepon函数库的。下面是其完整实现:
producer ()
{
while (1) {
// wait for interrupt from hardware here...
pthread_sleepon_lock ();
data_ready = 1;
pthread_sleepon_signal (&data_ready);
pthread_sleepon_unlock ();
}
}
可以看到,生产者线程同样也锁定了互斥体,以让其对data_ready标志量可以独占访问以设置其值。需要注意的是这里不是将1写入data_ready变量来唤醒客户的,而是pthread_sleepon_signal()函数唤醒的。
下面看看具体都发生了什么。我们对两个线程状态的缩写含义如下表所示:
| State | Meaning |
|---|---|
| CONDVAR | Waiting for the underlying condition variable associated with the sleepon |
| MUTEX | Waiting for a mutex |
| READY | Capable of using, or already using, the CPU |
| INTERRUPT | Waiting for an interrupt from the hardware |
具体过程如下:
| Action | Mutex owner | Consumer state | Producer state |
|---|---|---|---|
| Consumer locks mutex | Consumer | READY | INTERRUPT |
| Consumer examines data_ready | Consumer | READY | INTERRUPT |
| Consumer calls pthread_sleepon_wait() | Consumer | READY | INTERRUPT |
| pthread_sleepon_wait() unlocks mutex | Free | READY | INTERRUPT |
| pthread_sleepon_wait() blocks | Free | CONDVAR | INTERRUPT |
| Time passes | Free | CONDVAR | INTERRUPT |
| Hardware generates data | Free | CONDVAR | READY |
| Producer locks mutex | Producer | CONDVAR | READY |
| Producer sets data_ready | Producer | CONDVAR | READY |
| Producer calls pthread_sleepon_signal() | Producer | CONDVAR | READY |
| Consumer wakes up, pthread_sleepon_wait() tries to lock mutex | Producer | MUTEX | READY |
| Producer releases mutex | Free | MUTEX | READY |
| Consumer gets mutex | Consumer | READY | READY |
| Consumer processes data | Consumer | READY | READY |
| Producer waits for more data | Consumer | READY | INTERRUPT |
| Time passes (consumer processing) | Consumer | READY | INTERRUPT |
| Consumer finishes processing, unlocks mutex | Free | READY | INTERRUPT |
| Consumer loops back to top, locks mutex | Consumer | READY | INTERRUPT |
表中的最后一条重复了第一条,我们到此已经完成了一个整个的循环。
data_ready变量的目的是什么呢?它实际上有两个目的:
- 它是消费者与生产者线程之间的状态标志,用来标识系统状态。如果其为1,就表示有数据可用于处理;如果其为0,就表示没有数据,并且消费者线程应该处于阻塞状态;
- 它同时也是sleepon同步发生的地方。正式点说,data_ready的地址被作为唯一标识来使用,而它则作为sleepon锁的汇合对象。我们也可以使用(void *) 12345而不是&data_ready,只要这个标识是唯一与一致的,sleepon函数库是不管的。实际上,使用进程中一个变量的地址能够保证生成一个在进程中唯一的数字,当然了在一个进程中也不可能有两个变量使用同一个地址。
Related posts:
近期评论