7

它们是如何实现的,尤其是在 pthread 的情况下。他们在后台使用了哪些pthread同步 API?一点点伪代码将不胜感激。

4

2 回答 2

10

我已经有一段时间没有进行任何 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);
}

这就是为什么我将相关互斥体传递给队列操作的原因。

于 2012-06-15T05:12:55.470 回答
0

每个实现都可以不同,但​​由于 POSIX 要求线程能够多次获取 rwlock 上的读锁,因此它们通常必须默认偏爱读者。如果他们偏爱写入器,那么每当写入器等待时,读取器将在第二次读取锁尝试时死锁,除非实现可以确定读取器已经拥有读取锁,但唯一确定的方法是存储所有线程的列表持有读锁,这在时间和空间要求上非常低效。

于 2012-06-14T13:47:14.097 回答