繁体   English   中英

为什么 std::mutex 比 std::atomic 快?

[英]Why is std::mutex faster than std::atomic?

我想在多线程模式下将对象放入std::vector中。 所以我决定比较两种方法:一种使用std::atomic ,另一种使用std::mutex 我看到第二种方法比第一种方法快。 为什么?

我使用 GCC 4.8.1,在我的机器(8 个线程)上,我看到第一个解决方案需要391502微秒,而第二个解决方案需要175689微秒。

#include <vector>
#include <omp.h>
#include <atomic>
#include <mutex>
#include <iostream>
#include <chrono>

int main(int argc, char* argv[]) {
    const size_t size = 1000000;
    std::vector<int> first_result(size);
    std::vector<int> second_result(size);
    std::atomic<bool> sync(false);

    {
        auto start_time = std::chrono::high_resolution_clock::now();
        #pragma omp parallel for schedule(static, 1)
        for (int counter = 0; counter < size; counter++) {
            while(sync.exchange(true)) {
                std::this_thread::yield();
            };
            first_result[counter] = counter;
            sync.store(false) ;
        }
        auto end_time = std::chrono::high_resolution_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count() << std::endl;
    }

    {
        auto start_time = std::chrono::high_resolution_clock::now();
        std::mutex mutex; 
        #pragma omp parallel for schedule(static, 1)
        for (int counter = 0; counter < size; counter++) {
            std::unique_lock<std::mutex> lock(mutex);       
            second_result[counter] = counter;
        }
        auto end_time = std::chrono::high_resolution_clock::now();
        std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time).count() << std::endl;
    }

    return 0;
}

我认为您的问题不能参考标准互斥锁就可以回答,因为它们尽可能依赖于平台。 然而,有一件事,应该提到。

互斥锁并不慢。 您可能已经看过一些文章,将它们的性能与自定义自旋锁和其他“轻量级”东西进行了比较,但这不是正确的方法 - 这些不能互换。

自旋锁相当快,当它们在相对较短的时间内被锁定(获取)时 - 获取它们非常便宜,但是其他也在尝试锁定的线程在整个时间内都处于活动状态(在循环中不断运行) .

自定义自旋锁可以通过这种方式实现:

class SpinLock
{
private:
    std::atomic_flag _lockFlag;

public:
    SpinLock()
    : _lockFlag {ATOMIC_FLAG_INIT}
    { }

    void lock()
    {
        while(_lockFlag.test_and_set(std::memory_order_acquire))
        { }
    }

    bool try_lock()
    {
        return !_lockFlag.test_and_set(std::memory_order_acquire);
    }

    void unlock()
    {
        _lockFlag.clear();
    }
};

Mutex是一种原始类型,它要复杂得多。 特别是,在 Windows 上,我们有两个这样的原语 - 临界区,它在每个进程的基础上工作,而互斥体则没有这样的限制。

锁定互斥体(或临界区)的成本要高得多,但操作系统有能力真正让其他等待线程“休眠”,从而提高性能并帮助任务调度程序进行有效的资源管理。

我为什么写这个? 因为现代互斥体通常是所谓的“混合互斥体”。 当这种互斥锁被锁定时,它的行为就像一个普通的自旋锁——其他等待线程执行一定数量的“自旋”,然后锁定重互斥锁以防止浪费资源。

在您的情况下,互斥锁在每个循环迭代中被锁定以执行此指令:

second_result[counter] = omp_get_thread_num();

它看起来像一个快速的,所以“真正的”互斥锁可能永远不会被锁定。 这意味着,在这种情况下,您的“互斥锁”可以与基于原子的解决方案一样快(因为它本身就变成了基于原子的解决方案)。

此外,在第一个解决方案中,您使用了某种类似自旋锁的行为,但我不确定这种行为在多线程环境中是否可预测。 我很确定,“锁定”应该具有acquire语义,而解锁是release操作。 对于这个用例, Relaxed内存排序可能太弱了。


我编辑了代码以使其更加紧凑和正确。 它使用std::atomic_flag ,这是唯一的类型(与std::atomic<>不同),它保证是无锁的(即使std::atomic<bool>也不会给你)。

另外,请参阅下面关于“不屈服”的评论:这是特定情况和要求的问题。 自旋锁是多线程编程中非常重要的一部分,通常可以通过稍微修改其行为来提高其性能。 例如,Boost 库实现了spinlock::lock()如下:

void lock()
{
    for( unsigned k = 0; !try_lock(); ++k )
    {
        boost::detail::yield( k );
    }
}

来源: boost/smart_ptr/detail/spinlock_std_atomic.hpp

其中detail::yield()是(Win32 版本):

inline void yield( unsigned k )
{
    if( k < 4 )
    {
    }
#if defined( BOOST_SMT_PAUSE )
    else if( k < 16 )
    {
        BOOST_SMT_PAUSE
    }
#endif
#if !BOOST_PLAT_WINDOWS_RUNTIME
    else if( k < 32 )
    {
        Sleep( 0 );
    }
    else
    {
        Sleep( 1 );
    }
#else
    else
    {
        // Sleep isn't supported on the Windows Runtime.
        std::this_thread::yield();
    }
#endif
}

[来源: http : //www.boost.org/doc/libs/1_66_0/boost/smart_ptr/detail/yield_k.hpp]

首先,线程旋转固定的次数(在这种情况下为 4)。 如果互斥锁仍然被锁定,则使用pause指令(如果可用)或调用Sleep(0) ,这基本上会导致上下文切换并允许调度程序给另一个被阻塞的线程做一些有用的事情的机会。 然后,调用Sleep(1)来执行实际的(短的)睡眠。 非常好!

另外,这个声明:

自旋锁的目的是忙等待

不完全正确。 自旋锁的目的是作为一种快速、易于实现的锁原语——但它仍然需要正确编写,并考虑到某些可能的情况。 例如,英特尔说(关于 Boost 使用_mm_pause()作为在lock()内产生的方法):

在自旋等待循环中,内部暂停提高了代码检测锁释放的速度,并提供了特别显着的性能提升。

所以,像void lock() { while(m_flag.test_and_set(std::memory_order_acquire)); } void lock() { while(m_flag.test_and_set(std::memory_order_acquire)); }可能不像看起来那么好。

还有一个与您的问题相关的重要问题。 高效的自旋锁永远不会在涉及存储(例如exchangetest_and_set )的操作上“旋转”。 在典型的现代体系结构中,这些操作生成的指令要求具有锁定内存位置的缓存线处于独占状态,这非常耗时(尤其是当多个线程同时旋转时)。 始终在加载/只读时自旋,并仅在此操作有可能成功时才尝试获取锁。

例如,一篇很好的相关文章是:在 C++ 中正确实现自旋锁

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM