【问题标题】:Lightweight spinlocks built from GCC atomic operations?从 GCC 原子操作构建的轻量级自旋锁?
【发布时间】:2011-02-12 16:08:32
【问题描述】:

我想在我的项目中尽可能减少同步并编写无锁代码。当绝对必要时,我很想用原子操作构建的轻量级自旋锁代替 pthread 和 win32 互斥锁。我的理解是,这些是底层的系统调用,可能会导致上下文切换(这对于非常快速的关键部分可能是不必要的,只需旋转几次就可以了)。

我所指的原子操作在此处有详细记录:http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/Atomic-Builtins.html

这里有一个例子来说明我在说什么。想象一个可能有多个读取器和写入器的 RB-tree。 RBTree::exists() 是只读且线程安全的,RBTree::insert() 需要单个写入者(并且没有读取者)的独占访问才能安全。一些代码:

class IntSetTest
{
private:
    unsigned short lock;
    RBTree<int>* myset;

public:
    // ...

    void add_number(int n)
    {
        // Aquire once locked==false (atomic)
        while (__sync_bool_compare_and_swap(&lock, 0, 0xffff) == false);

        // Perform a thread-unsafe operation on the set
        myset->insert(n);

        // Unlock (atomic)
        __sync_bool_compare_and_swap(&lock, 0xffff, 0);
    }

    bool check_number(int n)
    {
        // Increment once the lock is below 0xffff
        u16 savedlock = lock;
        while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false)
            savedlock = lock;

        // Perform read-only operation    
        bool exists = tree->exists(n);

        // Decrement
        savedlock = lock;
        while (__sync_bool_compare_and_swap(&lock, savedlock, savedlock-1) == false)
            savedlock = lock;

        return exists;
    }
};

(假设它不需要是异常安全的)

这段代码真的是线程安全的吗?这个想法有什么优点/缺点吗?有什么建议吗?如果线程不是真正并发的,那么使用这样的自旋锁是不是一个坏主意?

提前致谢。 ;)

【问题讨论】:

  • 我在类似问题stackoverflow.com/questions/1919135/… 中给出的答案可能与此处相关。
  • 您的回答肯定与一般使用自旋锁的问题有关。在典型情况下,它们对于 smp 机器来说似乎是个好主意。最坏的情况(在关键部分停止运行的写入器)是否会与两个并发线程同时尝试插入的更可能的情况相同?在混合线程环境中,用户线程被映射到与机器上的逻辑处理器数量相等的内核线程数上呢?到那时,最坏的情况就更不可能了;没有?
  • 我不确定内核线程的数量在多大程度上影响了遇到性能问题的可能性。有可能编写器线程刚刚用完了锁进入和退出之间的时间片,这将导致无论有多少内核线程都出现问题。在这一点上,我会注意到 RB-tree 的插入操作是 O(log(n)),所以树越大,这个问题发生的可能性就越大。此外,较大的树更有可能在更新期间导致页面错误,这也会使问题案例更有可能发生。我会在这里避免使用自旋锁。
  • 正如我在 DeadMG 的回答中提到的,“轻量级”方面可能很重要。毫无疑问,互斥体的开销比 16 位值和一些原子操作要多得多。在“旋转”中添加 sched_yield() 或类似的调用会改善您的估计吗?

标签: c++ gcc multithreading pthreads thread-safety


【解决方案1】:

你需要lock 上的volatile 限定符,我也将它设为sig_atomic_t。如果没有 volatile 限定符,此代码:

    u16 savedlock = lock;
    while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false)
        savedlock = lock;

在 while 循环体中更新 savedlock 时可能不会重新读取 lock。考虑lock 为0xffff 的情况。然后,savedlock 将在检查循环条件之前为 0xffff,因此while 条件将在调用__sync_bool_compare_and_swap 之前短路。由于__sync_bool_compare_and_swap 没有被调用,编译器不会遇到内存屏障,所以它可以合理地假设lock 的值在你下面没有改变,并避免在savedlock 中重新加载它。

回复:sig_atomic_there 的讨论很不错。适用于信号处理程序的相同注意事项也适用于线程。

通过这些更改,我猜您的代码将是线程安全的。不过,我仍然建议使用互斥锁,因为您真的不知道在一般情况下您的 RB-tree 插入需要多长时间(根据我之前的问题中的 cmets)。

【讨论】:

  • 这很有趣。我读过很多文章解释为什么 volatile 是多线程程序最好的朋友,还有很多解释为什么 volatile 与此无关,让一切都变得 volatile 只会减慢程序的速度。在我的应用程序中,任何线程和任何时间都可以访问一半以上的数据。他们真的应该都是易变的吗?或者这是一个例外,因为它处于一个紧密的循环中,编译器可能会优化为只检查一次锁?
  • 即想象一个函数(不是内联的)被调用,检查一个变量,然后返回,然后再次快速调用。在这种情况下,是否不需要 volatile ,因为编译器无法跨多个调用优化代码?但是在上面的循环中它可能会意识到锁永远不会改变并优化它?所以 volatile 与缓存无关,它只是告诉编译器不要优化对内存的访问?我认为这对我来说很有意义。请确认或澄清! :)
  • 我花了一些时间来研究 volatile 的工作原理......简而言之,它的作用是防止优化内存访问,同时防止涉及 volatile 变量的内存操作的重新排序。 (涉及非易失性限定变量的内存操作可能会围绕涉及易失性的那些重新排序。此外,即使写入按顺序发生,不同的 CPU 可能会以不同的顺序注意到新值。)这对于多-线程同步在这种情况下,因为您还有__sync 例程提供内存屏障。
  • kernel.org/doc/Documentation/volatile-considered-harmful.txtkernel.org/doc/Documentation/atomic_ops.txt 中的更多背景信息。您只需要ACCESS_ONCE(x),Linux/GCC 将其定义为(*(volatile typeof(x)*)&amp;(x))。在读取之外使用volatile 存储类可能会助长误解。
【解决方案2】:

值得注意的是,如果您使用的是 Win32 互斥锁,那么从 Vista 开始,为您提供了一个线程池。根据您使用 RB 树的目的,您可以用它替换。

另外,您应该记住的是原子操作并不是特别快。微软说它们有几百个周期。

与其尝试以这种方式“保护”函数,不如简单地同步线程(更改为 SIMD/线程池方法,或者仅使用互斥锁)可能更有效。

但是,当然,如果没有看到您的代码,我真的无法再制作任何 cmets。多线程的问题在于您必须查看某人的整个模型才能理解它。

【讨论】:

  • 另一个重要的一点是它的整个“轻量级”方面。这只是一个例子,但在我的实际代码中,在某些情况下可能会有数百万个这样的对象,我认为创建数百万个 pthread 或 win32 互斥体并不实际。一个无符号的 16 位 int 实际上不会导致任何额外的开销(因为对齐)。
  • 其实线程池(msdn.microsoft.com/en-us/library/ms684957(VS.85).aspx)从Windows 2000开始就可以使用了。
  • 使用数百万个联锁操作也不实际。我仍然认为您需要重新设计线程模型。您似乎想设计一个高性能且完全不了解线程的类。 @Billy ONeal - 你是对的。我以前没有注意到这个功能。
  • 好吧。我确实需要设计一般来说既高性能又线程安全的类。这是因为我不知道它们将用于什么。这完全取决于用户。毫无疑问,如果我有一个特定的用途,就会有更有效的设计方法。
  • 啊 - 有道理,我没有考虑过。您无法编写适合线程模型的树,因为您不知道线程模型。在这种情况下,我真的没有什么可以帮助你的,只是你可以考虑把它变成用户的问题。
猜你喜欢
  • 2023-03-04
  • 2015-01-18
  • 1970-01-01
  • 2020-07-16
  • 2015-12-15
  • 2013-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多