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

Using Boost.Lockfree queue is slower than using mutexes

到目前为止,我一直在我的项目中使用 std::queue。我测量了此队列上的特定操作所需的平均时间。

时间是在两台机器上测得的:我的本地 Ubuntu VM 和一台远程服务器。 使用 std::queue,两台机器上的平均值几乎相同:~750 微秒。

然后我 "upgraded" 将 std::queue 更改为 boost::lockfree::spsc_queue,这样我就可以摆脱保护队列的互斥体。在我的本地 VM 上,我可以看到巨大的性能提升,现在平均为 200 微秒。然而,在远程机器上,平均值上升到 800 微秒,比以前慢了。

首先我认为这可能是因为远程机器可能不支持无锁实现:

来自Boost.Lockfree page:

Not all hardware supports the same set of atomic instructions. If it is not available in hardware, it can be emulated in software using guards. However this has the obvious drawback of losing the lock-free property.

要查明这些指令是否受支持,boost::lockfree::queue 有一个名为 bool is_lock_free(void) const; 的方法。 但是,boost::lockfree::spsc_queue 没有这样的功能,对我来说,这意味着它不依赖于硬件,并且始终是无锁的 - 在任何机器上。

性能下降的原因可能是什么?


示例代码 (Producer/Consumer)

// 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;
}

无锁算法通常比基于锁的算法表现更差。这是它们使用频率不高的一个关键原因。

无锁算法的问题在于它们通过允许竞争线程继续竞争来最大化竞争。锁通过解除竞争线程的调度来避免竞争。无锁算法,对于第一个近似值,只应在无法取消调度竞争线程时使用。这很少适用于应用程序级代码。

让我给你一个非常极端的假设。想象一下,四个线程 运行 在一个典型的现代双核 CPU 上运行。线程 A1 和 A2 正在操作集合 A。线程 B1 和 B2 正在操作集合 B。

首先,让我们假设集合使用锁。这意味着如果线程 A1 和 A2(或 B1 和 B2)同时尝试 运行,其中一个将被锁阻塞。因此,很快,一个 A 线程和一个 B 线程将 运行ning。这些线程将 运行 非常快并且不会争用。任何时候线程试图争用,冲突的线程将被取消调度。耶。

现在,想象一下集合不使用锁。现在,线程 A1 和 A2 可以同时 运行。这将导致不断的争用。集合的缓存行将在两个核心之间来回切换。核心间总线可能会饱和。性能会很糟糕。

同样,这被高度夸大了。但是你明白了。您想避免争用,而不是尽可能多地忍受争用。

但是,现在 运行 再次进行此思想实验,其中 A1 和 A2 是整个系统上唯一的线程。现在,无锁集合可能更好(尽管您可能会发现在这种情况下最好只有一个线程!)。

几乎每个程序员都经历过这样一个阶段,他们认为锁是不好的,避免使用锁可以使代码运行得更快。最终,他们意识到 争用 使事情变得缓慢和锁定,正确使用,最大限度地减少争用。

我不能说 boost 无锁队列在所有可能的情况下都比较慢。根据我的经验, push(const T& item) 正在尝试制作副本。如果您正在构建 tmp 对象并推入队列,那么您将受到性能拖累。我认为库只需要重载版本 push(T&& item) 来使可移动对象更有效率。在添加新功能之前,您可能不得不使用指针、普通类型或 C++11 之后提供的智能类型。这是队列的一个相当有限的方面,而且我很少使用无锁队列。