它们是如何实现的,尤其是在 pthread 的情况下。他们在后台使用了哪些pthread
同步 API?一点点伪代码将不胜感激。
2 回答
我已经有一段时间没有进行任何 pthreads 编程了,但是当我这样做时,我从未使用过 POSIX 读/写锁。问题是大多数时候互斥体就足够了:即。您的关键部分很小,并且该区域的性能不是那么关键,以至于双重障碍值得担心。
在那些性能问题的情况下,通常使用原子操作(通常作为编译器扩展提供)是更好的选择(即额外的障碍是问题,而不是关键部分的大小)。
当您消除所有这些情况时,您会遇到需要真正 rw-lock 的特定性能/公平性/rw-bias 要求的情况;那就是当您发现 POSIX rw-lock 的所有相关性能/公平性参数都未定义且特定于实现时。在这一点上,您通常最好实现自己的,这样您就可以确保满足适当的公平/读写偏差要求。
基本算法是计算每个线程中有多少处于临界区,如果还不允许访问线程,则将其分流到适当的队列以等待。您的大部分工作将是在服务两个队列之间实施适当的公平/偏见。
以下类似 C 的 pthreads 类伪代码说明了我要说的内容。
struct rwlock {
mutex admin; // used to serialize access to other admin fields, NOT the critical section.
int count; // threads in critical section +ve for readers, -ve for writers.
fifoDequeue dequeue; // acts like a cond_var with fifo behaviour and both append and prepend operations.
void *data; // represents the data covered by the critical section.
}
void read(struct rwlock *rw, void (*readAction)(void *)) {
lock(rw->admin);
if (rw->count < 0) {
append(rw->dequeue, rw->admin);
}
while (rw->count < 0) {
prepend(rw->dequeue, rw->admin); // Used to avoid starvation.
}
rw->count++;
// Wake the new head of the dequeue, which may be a reader.
// If it is a writer it will put itself back on the head of the queue and wait for us to exit.
signal(rw->dequeue);
unlock(rw->admin);
readAction(rw->data);
lock(rw->admin);
rw->count--;
signal(rw->dequeue); // Wake the new head of the dequeue, which is probably a writer.
unlock(rw->admin);
}
void write(struct rwlock *rw, void *(*writeAction)(void *)) {
lock(rw->admin);
if (rw->count != 0) {
append(rw->dequeue, rw->admin);
}
while (rw->count != 0) {
prepend(rw->dequeue, rw->admin);
}
rw->count--;
// As we only allow one writer in at a time, we don't bother signaling here.
unlock(rw->admin);
// NOTE: This is the critical section, but it is not covered by the mutex!
// The critical section is rather, covered by the rw-lock itself.
rw->data = writeAction(rw->data);
lock(rw->admin);
rw->count++;
signal(rw->dequeue);
unlock(rw->admin);
}
类似上面的代码是任何 rwlock 实现的起点。考虑一下问题的性质,并用适当的逻辑替换出队,以确定接下来应该唤醒哪类线程。根据应用程序,通常允许有限数量/时间的读者超越作者,反之亦然。
当然,我的一般偏好是完全避免 rw-locks。通常通过使用原子操作、互斥体、STM、消息传递和持久数据结构的某种组合。然而,有时候你真正需要的是一个 rw-lock,当你这样做时,了解它们是如何工作的很有用,所以我希望这会有所帮助。
编辑 - 回答(非常合理的)问题,我在上面的伪代码中在哪里等待:
我假设出队实现包含等待,因此在某处append(dequeue, mutex)
或prepend(dequeue, mutex)
存在以下代码块:
while(!readyToLeaveQueue()) {
wait(dequeue->cond_var, mutex);
}
这就是为什么我将相关互斥体传递给队列操作的原因。
每个实现都可以不同,但由于 POSIX 要求线程能够多次获取 rwlock 上的读锁,因此它们通常必须默认偏爱读者。如果他们偏爱写入器,那么每当写入器等待时,读取器将在第二次读取锁尝试时死锁,除非实现可以确定读取器已经拥有读取锁,但唯一确定的方法是存储所有线程的列表持有读锁,这在时间和空间要求上非常低效。