【问题标题】:Implementing critical section实施临界区
【发布时间】:2012-01-30 16:14:42
【问题描述】:

什么方法更好更快地创建临界区?

在 sem_wait 和 sem_post 之间使用二进制信号量。
或者使用原子操作:

#include <sched.h>

void critical_code(){
    static volatile bool lock = false;

    //Enter critical section
    while ( !__sync_bool_compare_and_swap (&lock, false, true ) ){
        sched_yield();
    }

    //...

    //Leave critical section
    lock = false;
}

【问题讨论】:

  • 个人资料!轮廓!个人资料!......

标签: c multithreading gcc atomic critical-section


【解决方案1】:

无论你使用什么方法,你的代码最糟糕的性能问题与你使用什么类型的锁无关,而是你锁定的是代码而不是数据。

话虽如此,没有理由像这样滚动您自己的自旋锁。如果你想要一个自旋锁,要么使用pthread_spin_lock,要么使用pthread_mutex_locksem_wait(带有二进制信号量),如果你想要一个在竞争时可以让给其他进程的锁。您编写的代码在使用sched_yield 方面是两全其美。对sched_yield 的调用将确保锁在同时存在锁争用和cpu 负载的情况下等待至少几毫秒(并且可能是整个调度时间片),并且当存在争用但没有cpu 时它将消耗100% cpu负载(例如,由于锁持有人在 IO 中被阻塞)。如果您想获得自旋锁的任何好处,您需要在不进行任何系统调用的情况下进行自旋。如果您想要让出 cpu 的任何好处,您应该使用适当的同步原语,它将使用(在 Linux 上)futex(或等效的)操作来精确地让出,直到锁可用 - 不会更短,也不会不再。

如果碰巧这一切都超出了您的想象,那就不要考虑编写自己的锁了..

【讨论】:

    【解决方案2】:

    如果锁的争用很少和/或从不长时间持有,自旋锁的性能会更好。否则,最好使用阻塞而不是旋转的锁。当然也有混合锁,它会旋转几次,如果无法获取锁,就会阻塞。

    哪个更适合您取决于您​​的应用程序。只有你能回答这个问题。

    【讨论】:

    • 临界区不够小,可能会做I/O操作。如何使用混合锁?
    • 如果在持有锁时可以执行任何 IO,唯一有意义的锁类型是在争用时休眠的锁。当锁的持续时间超过几个周期时,自旋锁是一种可怕的方法。例如,spin_lock; counter++; spin_unlock; 是有意义的。 spin_lock; write(foo); spin_unlock; 没有。
    【解决方案3】:

    您对 gcc 文档的了解不够深入。此类锁的正确内置函数是 __sync_lock_test_and_set__sync_lock_release。这些正是你需要做这样的事情的保证。根据新的 C11 标准,这将是 atomic_flag 类型,操作为 atomic_flag_test_and_setatomic_flag_clear

    正如 R. 已经指出的那样,将 sched_yield 放入循环中确实是个坏主意。

    如果临界区内部的代码只有几个循环,那么它的执行落到调度片边界的概率很小。将被阻止主动旋转的线程数最多为处理器数减一。如果您在没有立即获得锁的情况下立即放弃执行,那么所有这些都不成立。如果你对你的锁收益有真正的竞争,你将有大量的上下文切换,这将使你的系统几乎陷入停顿。

    【讨论】:

    • 我没有看到任何令人信服的理由来使用这些原语而不是比较和交换。它们可能在一些非常奇怪的机器上更有效(没有现代机器),但我宁愿确保所有拱门上的锁语义相同,并且能够做有用的事情,比如在原子锁字段本身中存储所有者 ID 或其他信息。
    • 使用atomic_flag的原因很简单。它是 C11 中唯一保证无锁的原子类型。因此,它是唯一保证信号安全且在操作中间取消调度时不会阻塞其他线程的此类类型。
    • 现在您已经引入了 C11 标准原子而不是 GCC 原子。据我所知,所有 GCC 都保证实际上是原子的,因为它们对未修改类型的对象进行操作,而不是 _Atomic int 等可能大于相应的非原子类型以允许额外的锁定字段.无论如何,我认为这是一个可以在很大程度上忽略的主要实施质量问题。
    • 永远不会有一个 C11 实现(或者至少不是一个受人尊敬的实现),其中“原子”实际上具有锁定字段,因为它们在缺乏它们的 CPU 上模拟真实原子的方式要高效得多;查看在 Linux ARM 上使用内核帮助程序实现原子比较和交换的方式。 (这种方法不适用于 SMP,但任何专为 SMP 使用而设计的 CPU 都将拥有一套完整的工作原子。)
    • @R.. 我认为你错了。在 C11 中,_Atomic 是适用于任何数据类型的限定符。因此,实现必须提供任何数据类型的无锁版本。如果没有锁定字段,我不明白你怎么能做到这一点。对于 ARM,正如您给出的示例,这首先要实现 uint64_t 的无锁版本。并非该处理器的所有版本都有本机指令。如果必须使用锁定字段,最好使用具有正确内存排序语义的字段。
    【解决方案4】:

    正如其他人所指出的,这并不是关于锁定代码的速度。这是因为一旦使用“xchg reg,mem”启动锁定序列,锁定信号就会通过缓存向下发送到所有总线上的设备。当最后一个设备确认它将保持并确认这一点时 - 这可能需要数百个如果不是一千个时钟周期,则执行实际的交换。如果您最慢的设备是经典的 PCI 卡,它的总线速度将是 33 MHz,大约是 CPU 内部时钟的百分之一。 PCI 设备(如果处于活动状态)将需要几个时钟周期(@33 MHz)来响应。在此期间,CPU 将等待确认返回。

    大多数自旋锁可能用在设备驱动程序中,其中例程不会被操作系统抢占,但可能会被更高级别的驱动程序中断。

    临界区实际上只是一个自旋锁,但它与操作系统有接口,因为它可能被抢占。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-07-30
      • 2018-10-17
      • 2013-03-26
      • 2013-04-07
      • 2021-09-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多