使用Boost.Lockfree队列比使用互斥锁慢

63
到目前为止,在我的项目中我一直在使用std::queue。我测量了针对此队列的特定操作所需的平均时间。
这些时间是在2台机器上测量的:我的本地Ubuntu虚拟机和远程服务器。 使用std::queue,两台机器的平均值几乎相同:约为750微秒。
然后我将std::queue“升级”到boost::lockfree::spsc_queue,以便摆脱保护队列的互斥锁。在我的本地VM上,我可以看到巨大的性能提升,平均值现在是200微秒。但是在远程机器上,平均值却增加到800微秒,比以前慢。
起初,我认为这可能是因为远程机器可能不支持无锁实现:
Boost.Lockfree页面: 引用:

并非所有硬件都支持相同的原子指令集。如果硬件中没有它,则可以使用守卫在软件中模拟它。但是这显然会失去无锁属性。

要找出这些指令是否受支持,boost::lockfree::queue有一个名为bool is_lock_free(void) const;的方法。 但是,boost::lockfree::spsc_queue没有像这样的函数,这意味着它不依赖于硬件,并且总是无锁的-在任何机器上。
那么性能下降的原因是什么?

示例代码(生产者/消费者)

// c++11 compiler and boost library required

#include <iostream>
#include <cstdlib>
#include <chrono>
#include <async>
#include <thread>
/* Using blocking queue:
 * #include <mutex>
 * #include <queue>
 */
#include <boost/lockfree/spsc_queue.hpp>


boost::lockfree::spsc_queue<int, boost::lockfree::capacity<1024>> queue;

/* Using blocking queue:
 * std::queue<int> queue;
 * std::mutex mutex;
 */

int main()
{
    auto producer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Producing data in a random interval
        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            // Push random int (0-9999)
            queue.push(std::rand() % 10000);

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for random duration (0-999 microseconds)
            std::this_thread::sleep_for(std::chrono::microseconds(rand() % 1000));
        }
    }

    auto consumer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Example operation on the queue.
        // Checks if 1234 was generated by the producer, returns if found.

        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            int value;
            while(queue.pop(value)
            {
                if(value == 1234)
                    return;
            }

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for 100 microseconds
            std::this_thread::sleep_for(std::chrono::microseconds(100));
        }
    }

    consumer.get();
    std::cout << "1234 was generated!" << std::endl;
    return 0;
}

1
请考虑添加一个 [mcve],以便重现您的性能测量。这将有助于提供更实用的答案。 - Zulan
@Zulan 我会尽快添加一个具体的例子。 - Bobface
2
这个例子可能存在性能问题,因为在多线程环境中不应该使用 rand。应该使用 C++11 的 <random> 或者 rand_r参见。你如何计时代码并且它如何对应你的性能观察? - Zulan
2
最好以建模的方式而不是使用Websockets。这样更容易复现。但是,示例必须展示您观察到的性能异常。请确保在示例中包括性能观察的描述以及涉及的系统。 - Zulan
1
你的假设,即缺少is_lock_free就意味着什么是不正确的。在实现中,我们可以看到它们都使用原子操作,这些操作直接依赖于CPU支持。 - yano
显示剩余3条评论
2个回答

154

无锁算法通常比有锁算法的性能差。这是它们不被频繁使用的一个重要原因。

无锁算法的问题在于它通过允许竞争线程继续竞争来最大化争用。而锁通过取消调度竞争线程来避免争用。从第一次近似来看,无锁算法只有在无法取消调度竞争线程时才应该使用,而这种情况很少适用于应用级代码。

让我举个非常极端的例子。想象四个线程在运行一个典型的、现代的双核 CPU 上。线程 A1 和 A2 正在操作集合 A。线程 B1 和 B2 正在操作集合 B。

首先,让我们想象这些集合使用锁。这意味着如果线程 A1 和 A2(或 B1 和 B2)尝试同时运行,其中一个将会被锁住。所以,很快,一个 A 线程和一个 B 线程将会运行。这些线程将会很快地运行而不会竞争。任何时候线程尝试竞争,冲突的线程将被取消调度。太好了。

现在,想象集合不使用锁。现在,线程 A1 和 A2 可以同时运行。这将会导致持续的争用。集合的缓存行将在两个核心之间来回反弹。芯片之间的总线可能会被占满。性能将是可怕的。

再次说明,这是高度夸张的。但是你明白了。你想避免争用,而不是尽可能地忍受它。

然而,现在再运行这个思想实验,其中 A1 和 A2 是整个系统上唯一的线程。现在,无锁集合可能更好(虽然在这种情况下,只有一个线程可能更好!)。

几乎每个程序员都经历过一段时间,他们认为锁是不好的,并且避免锁可以使代码运行得更快。最终,他们意识到是 争用 使事情变慢,而锁,如果正确使用,可以最小化争用。


11
非常好的标准答案。时间窗口过后我会设置悬赏。 - sehe
3
嗯,我想我目前处于避免锁定的阶段:p,是否有其他资源/书籍可以阅读,涉及这样的主题? - Abhinav Gauniyal
3
阻塞锁实现和超负载核心可以减少对不同锁的争用。但是,从提供的问题信息来看,我不确定这是否适用。 - Zulan
4
这个问题关于无锁队列与互斥锁的特定性能观察。虽然在一般情况下考虑竞争当然是非常有效的,但我认为这个问题并没有强烈的XY问题的迹象。我对第一句话表示怀疑——用另一个概括替换原来的概括——我不认为你的论点支持它。你的论点基本上是说,重要的是竞争,而不是避免数据竞争的具体机制,阻塞式的锁实现可以在某些情况下减轻竞争。 - Zulan
4
您的回答是否能解释为什么无锁队列在一个系统上提高了275%的速度,而在另一个系统上却减慢了6%?(我认为这个问题缺乏足够信息,无法作出确切的回答,只能进行猜测。) - Zulan
显示剩余6条评论

0

我不能说boost的无锁队列在所有情况下都较慢。在我的经验中,push(const T& item) 正在尝试进行复制。如果您正在构造tmp对象并将其推入队列,则会受到性能拖延的影响。我认为该库只需要过载版本 push(T&& item) 来使可移动对象更有效。在添加新函数之前,您可能必须使用指针、普通类型或C++11后提供的智能类型。这是队列的一个相当有限的方面,我很少使用无锁队列。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接