想象一下,你有一个许多线程使用的键值映射数据结构。如果没有写入数据结构,多个线程应该能够同时查找(读取)值。作者不是那么合群 - 为了避免数据损坏,一次只有一个线程可以修改(write
)数据结构(当时没有读者可能正在阅读)。
这是 _ 读写器问题 _ 的一个例子。也就是说,我们如何有效地同步多个读者和作者,以便多个读者可以一起阅读,但作家获得独家访问?
下面显示了不正确的尝试(“lock”是pthread_mutex_lock
的简写):
|
read() {
lock(&m)
// do read stuff
unlock(&m)
}
|
write() {
lock(&m)
// do write stuff
unlock(&m)
}
|
至少我们的第一次尝试不会遭受数据损坏(读者必须在作家写作时等待,反之亦然)!但读者也必须等待其他读者。那么让我们尝试另一种实现..
|
read() {
while(writing) {/*spin*/}
reading = 1
// do read stuff
reading = 0
}
|
write() {
while(reading || writing) {/*spin*/}
writing = 1
// do write stuff
writing = 0
}
|
我们的第二次尝试遭遇竞争条件 - 想象两个线程是否同时调用read
和write
(或两者都称为写入)。两个线程都可以继续!其次,我们可以有多个读者和多个作者,所以让我们跟踪读者或作者的总数。这让我们尝试#3,
请记住pthread_cond_wait
执行 _ 三个 _ 动作。首先,它以原子方式解锁互斥锁然后休眠(直到它被pthread_cond_signal
或pthread_cond_broadcast
唤醒)。第三,唤醒线程必须在返回之前重新获取互斥锁。因此,实际上只有一个线程可以在 lock 和 unlock()方法定义的临界区内运行。
下面的实现#3 确保如果有任何作者写作,读者将进入 cond_wait。
read() {
lock(&m)
while (writing)
cond_wait(&cv, &m)
reading++;
/* Read here! */
reading--
cond_signal(&cv)
unlock(&m)
}
然而,只有一个读者一次可以阅读,因为候选人#3 没有解锁互斥锁。更好的版本在阅读之前解锁:
read() {
lock(&m);
while (writing)
cond_wait(&cv, &m)
reading++;
unlock(&m)
/* Read here! */
lock(&m)
reading--
cond_signal(&cv)
unlock(&m)
}
这是否意味着作者和阅读可以同时读写?没有!首先,请记住 cond_wait 要求线程在返回之前重新获取互斥锁。因此,一次只有一个线程可以在临界区内执行代码(用**标记)!
read() {
lock(&m);
** while (writing)
** cond_wait(&cv, &m)
** reading++;
unlock(&m)
/* Read here! */
lock(&m)
** reading--
** cond_signal(&cv)
unlock(&m)
}
作家必须等待每个人。锁定可确保相互排斥。
write() {
lock(&m);
** while (reading || writing)
** cond_wait(&cv, &m);
** writing++;
**
** /* Write here! */
** writing--;
** cond_signal(&cv);
unlock(&m);
}
上面的候选人#3 也使用pthread_cond_signal
;这只会唤醒一个线程。例如,如果许多读者等待作者完成,那么只有一个睡觉的读者会从他们的睡眠中醒来。读写器应该使用cond_broadcast
,以便唤醒所有线程并检查它们的 while 循环条件。
上面的候选人#3 患有饥饿。如果读者经常到达,那么作家永远无法继续(“阅读”计数永远不会减少到零)。这被称为 _ 饥饿 _,并将在重负荷下被发现。我们的解决方法是为作者实现有限等待。如果作家到了,他们仍然需要等待现有的读者,但是未来的读者必须被置于“握笔”中并等待作者完成。 “握笔”可以使用变量和条件变量来实现(这样我们就可以在编写完成后唤醒线程)。
我们的计划是,当作家到来时,在等待当前读者完成之前,记录我们的写作意图(通过递增计数器“作者”)。草绘如下 -
write() {
lock()
writer++
while (reading || writing)
cond_wait
unlock()
...
}
当作家非零时,传入的读者将不被允许继续。注意'作家'表示作家已到达,而'阅读'和'写'计数器表示有 _ 有效 _ 读者或作者。
read() {
lock()
// readers that arrive *after* the writer arrived will have to wait here!
while(writer)
cond_wait(&cv,&m)
// readers that arrive while there is an active writer
// will also wait.
while (writing)
cond_wait(&cv,&m)
reading++
unlock
...
}
以下是我们对 Reader-Writer 问题的第一个解决方案。请注意,如果您继续阅读“Reader Writer 问题”,那么您会发现我们通过给予编写者优先访问锁来解决“第二个 Reader Writer 问题”。该解决方案不是最佳的。然而,它满足了我们原来的问题(N 个活跃的读者,单个活跃的作家,如果有一个持续的读者流,避免作家的饥饿)。
你能找出任何改进吗?例如,你如何改进代码,以便我们只唤醒读者或一个作家?
int writers; // Number writer threads that want to enter the critical section (some or all of these may be blocked)
int writing; // Number of threads that are actually writing inside the C.S. (can only be zero or one)
int reading; // Number of threads that are actually reading inside the C.S.
// if writing !=0 then reading must be zero (and vice versa)
reader() {
lock(&m)
while (writers)
cond_wait(&turn, &m)
// No need to wait while(writing here) because we can only exit the above loop
// when writing is zero
reading++
unlock(&m)
// perform reading here
lock(&m)
reading--
cond_broadcast(&turn)
unlock(&m)
}
writer() {
lock(&m)
writers++
while (reading || writing)
cond_wait(&turn, &m)
writing++
unlock(&m)
// perform writing here
lock(&m)
writing--
writers--
cond_broadcast(&turn)
unlock(&m)
}