【问题标题】:Fair critical section (Linux)公平临界区 (Linux)
【发布时间】:2011-09-20 22:34:01
【问题描述】:

在多线程 Linux 应用程序中,我将互斥锁用于关键部分。除了公平问题外,这非常有效。一个线程离开临界区并立即重新进入可能不会给任何其他线程机会。例如

while(true)
{
    critsect.enter();
    ... do calculations ...
    ... maybe call a blocking operation so we sleep ...
    critsect.leave();
}

很可能会阻止任何其他线程进入相同的临界区。互斥是不公平的。

是否有解决方案来制作公平的关键部分?我正在考虑添加一个队列,以便关键部分按照它们的“到达”顺序执行。或者,如果其他线程正在等待,至少有一个计数器可以在解锁后执行 pthread_yield()。

对于这种要求有推荐的做法吗?

【问题讨论】:

  • 您能否发布有关您的公平声明的权威资源。恕我直言,我认为It can happen that a thread leaving a critical section and re-entering right away does not give any other thread a chance 不是真的。
  • 当互斥锁被解锁时,线程继续运行。特别是。当在解锁之前有一个线程切换(例如阻塞操作)时,线程极有可能不间断地进入下一个锁。现在,pthread_mutex 是一个非排队互斥体,所以即使其他一些(当前不活动的)线程在锁定调用上阻塞,我也可以锁定它。我没有明确的资源来记录这一点,但 stackoverflow 问题 3045066 显示了一个示例,您可以直接运行它(在单核系统上)。
  • 在临界区阻塞或休眠不是一个好主意,因为其他线程无法利用这个休眠时间轮到它

标签: linux pthreads mutex critical-section


【解决方案1】:

您可以在 pthreads 互斥锁之上构建一个 FIFO“票证锁”,如下所示:

#include <pthread.h>

typedef struct ticket_lock {
    pthread_cond_t cond;
    pthread_mutex_t mutex;
    unsigned long queue_head, queue_tail;
} ticket_lock_t;

#define TICKET_LOCK_INITIALIZER { PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER }

void ticket_lock(ticket_lock_t *ticket)
{
    unsigned long queue_me;

    pthread_mutex_lock(&ticket->mutex);
    queue_me = ticket->queue_tail++;
    while (queue_me != ticket->queue_head)
    {
        pthread_cond_wait(&ticket->cond, &ticket->mutex);
    }
    pthread_mutex_unlock(&ticket->mutex);
}

void ticket_unlock(ticket_lock_t *ticket)
{
    pthread_mutex_lock(&ticket->mutex);
    ticket->queue_head++;
    pthread_cond_broadcast(&ticket->cond);
    pthread_mutex_unlock(&ticket->mutex);
}

在这种方案下,当线程处于票据锁保护的临界区时,不会持有低级 pthreads 互斥锁,从而允许其他线程加入队列。

【讨论】:

  • 是否应该将 TICKET_LOCK_INITIALIZER 更新为 {PTHREAD_COND_INITIALIZER, PTHREAD_MUTEX_INITIALIZER, 0, 0 }?好像这样会更完整...
  • @HighExodus:可以,但没必要 - 对象永远不会在 C 中部分初始化,任何没有显式初始化器的聚合成员都被初始化为适当类型的零。
  • 还有一个额外的好处是可以将票号写入调试日志,这可以帮助某人进行调查,例如死锁。
【解决方案2】:

即使有一个公平的临界区,代码的性能也可能很糟糕,因为如果临界区被长时间持有,线程将经常等待它。

因此,我建议您尝试重构代码,使其无需在较长时间内锁定关键部分。要么完全使用不同的方法(通常建议通过消息队列传递对象,因为它很容易做到正确),要么至少通过在不持有锁的情况下对局部变量进行大部分计算,而不是只使用锁来存储结果。如果锁的持有时间较短,线程将花费更少的时间等待它,这通常会提高性能并使公平性不再是问题。您也可以尝试增加锁定粒度(单独锁定较小的对象),这也将减少争用。

编辑:好的,仔细想想,我相信 Linux 中的每个关键部分都是大致公平的。每当有睡眠者时,解锁操作必须进入内核告诉它唤醒它们。在从内核返回期间,调度程序运行并选择具有最高优先级的进程。睡眠者在等待时优先级上升,因此在某些时候它们会足够高以至于释放会导致任务切换。

【讨论】:

  • 是的,这当然是一个真实的说法。然而它并没有真正解决问题,因为即使关键部分很小,如果我在一个循环中调用它们(这甚至不必是一个紧密的循环,因为线程调度不是在微秒粒度)相同可能会出现问题。在我的特殊情况下,由于受保护代码的性质,不可能进行太多重组。无论哪种方式,问题都是如何获得公平的关键部分。
  • 让我怀疑您是否需要在临界区中放置“做一些计算”。只需复制该部分中您需要的少量数据,然后在外部计算(但在循环中)。
  • 好吧,如果你锁定一个循环,但只持有一小部分的锁,那么另一个线程必须等待的概率无论如何都会降低。但是如果你不能重构代码,那么你需要一个公平的关键部分。仔细想想,我还真信了。
【解决方案3】:

如果您的主张成立(我没有时间阅读,而且您似乎在发布问题之前已经对此进行了研究),我建议

 sleep(0);

在关键部分之间显式地让步。

while(true)
{
    critsect.enter();
    ... do calculations ...
    ... maybe call a blocking operation so we sleep ...
    critsect.leave();
    sleep(0);
}

【讨论】:

  • 嗯,呵呵 :-) 但我不想这样做,因为这种情况只发生在大约 20% 的情况下。因此,每次让出 CPU 都会使线程无法分配 CPU 时间,从而降低性能。
【解决方案4】:

好的,这个怎么样:

while(true)
{
    sema.wait;
    critsect.enter();
    sema.post;
    ... do calculations ...
    ... maybe call a blocking operation so we sleep ...
    critsect.leave();
}

初始化。信号量计数为 1。在尝试获取 CS 并在完成时发出信号之前,让其他线程也等待信号量。如果“计算”线程获得 sema,它可以到达 CS 并锁定它。一旦进入锁,但在长 claculate 之前,sema 会发出信号,然后另一个线程可以到达 CS 但不能进入它。当“计算”线程退出锁时,它不能循环并重新锁定它,因为 sema. count 为零,所以另一个线程获得了锁。 “计算”线程必须等待 sema,直到进入的另一个线程完成其访问并发出 sema 信号。

通过这种方式,另一个线程可以“保留”对数据的访问,即使它实际上还不能访问它。

Rgds, 马丁

【讨论】:

  • 好的,所以我的代码格式搞砸了
  • while(true) { critsect.enter(); ... do calculations ... ... maybe call a blocking operation so we sleep ... critsect.leave(); sleep(0); }
  • 有些日子,最好不要起床 :(( 我尝试在我之前的评论中添加反引号代码,但它看起来仍然是错误的 :(
【解决方案5】:

恕我直言,您可以在 Linux 上使用 FIFO 调度器并更改线程的优先级:

thread_func() {
    ... 
    pthread_t t_id = pthread_self();
    struct sched_param prio_zero, prio_one;
    prio_zero.sched_priority = sched_get_priority_min(SCHED_FIFO);
    prio_one.sched_priority = sched_get_priority_min(SCHED_FIFO) + 1;
    phtread_setschedparam(t_id, SCHED_FIFO, &prio_zero);
    ...
    while(true)
    {
        ... Doing something before
        phtread_setschedparam(t_id, SCHED_FIFO, &prio_one);
        critsect.enter();
        ... do calculations ...
        ... maybe call a blocking operation so we sleep ...
        critsect.leave();
        phtread_setschedparam(t_id, SCHED_FIFO, &prio_zero);
        ... Do something after
    }
}

【讨论】:

    猜你喜欢
    • 2014-06-24
    • 1970-01-01
    • 2017-12-23
    • 2018-10-17
    • 2013-03-26
    • 2013-04-07
    • 2021-09-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多