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

C++ 为什么std::mutex比std::atomic快?,c++,mutex,atomic,C++,Mutex,Atomic,我想将对象置于多线程模式下的std::vector。所以我决定比较两种方法:一种使用std::atomic,另一种使用std::mutex。我发现第二种方法比第一种更快。为什么? 我使用GCC 4.8.1,在我的机器上(8个线程),我看到第一个解决方案需要391502微秒,第二个解决方案需要175689微秒 #include <vector> #include <omp.h> #include <atomic> #include <mutex> #

我想将对象置于多线程模式下的
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;
}
#包括
#包括
#包括
#包括
#包括
#包括
int main(int argc,char*argv[]){
常数大小=1000000;
std::向量第一个结果(大小);
std::矢量第二次_结果(大小);
std::原子同步(假);
{
自动启动时间=标准::时钟::高分辨率时钟::现在();
#计划的pragma omp并行(静态,1)
用于(int计数器=0;计数器<大小;计数器++){
while(sync.exchange(true)){
std::this_thread::yield();
};
第一个结果[计数器]=计数器;
同步存储(假);
}
自动结束时间=标准::时钟::高分辨率时钟::现在();

std::cout我不认为你的问题可以回答,只提到标准-互斥锁尽可能依赖于平台。但是,有一件事应该提到

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

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

自定义旋转锁可以通过以下方式实现:

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();
    }
};
互斥是一个原语,它要复杂得多。特别是在Windows上,我们有两个这样的原语-,它们在每个进程的基础上工作,并且没有这样的限制

锁定互斥锁(或关键部分)的成本要高得多,但操作系统能够真正将其他等待线程置于“睡眠”状态,从而提高性能并帮助任务调度器进行有效的资源管理

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

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

second_result[counter] = omp_get_thread_num();
它看起来很快,所以“真正的”互斥锁可能永远不会被锁定。这意味着,在这种情况下,您的“互斥锁”可以和基于原子的解决方案一样快(因为它本身就是基于原子的解决方案)

另外,在第一个解决方案中,您使用了某种类似自旋锁的行为,但我不确定这种行为在多线程环境中是否可以预测应该具有
acquire
语义,而解锁是
release
op.
released
内存顺序对于这个用例来说可能太弱了


我对代码进行了编辑,使其更加紧凑和正确。它使用了,这是唯一一种保证无锁的类型(与
std::atomic
specializations不同)(即使
std::atomic
也不提供)

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

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

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

inline void yield(无符号k)
{
if(k<4)
{
}
#如果已定义(增压\u SMT\u暂停)
else if(k<16)
{
增强\u SMT\u暂停
}
#恩迪夫
#如果!BOOST\u平台\u WINDOWS\u运行时
else if(k<32)
{
睡眠(0);
}
其他的
{
睡眠(1);
}
#否则
其他的
{
//Windows运行时不支持睡眠。
std::this_thread::yield();
}
#恩迪夫
}
[来源:

首先,线程旋转固定次数(本例中为4次)。如果互斥锁仍处于锁定状态(如果可用),则调用
Sleep(0)
,这基本上会导致上下文切换,并允许调度程序为另一个被阻止的线程提供执行有用操作的机会。然后,调用
Sleep(1)
,以执行实际操作(短)睡吧,很好

此外,本声明:

自旋锁的作用是忙着等待

并非完全正确。spinlock的目的是作为一个快速、易于实现的锁原语——但它仍然需要正确编写,并考虑到某些可能的情况。例如,(将Boost使用
\u mm\u pause()
作为一种屈服于内部
锁()
的方法):

在自旋等待循环中,暂停内在提高了 该代码检测锁的释放,并提供 显著的性能提升

所以,像这样的实现
void lock(){while(m_flag.test_and_set(std::memory_order_acquire))}

可能没有看上去那么好。

还有一个与您的问题相关的重要问题。高效的自旋锁在涉及存储的操作(例如
exchange
test\u and\u set
)上不会“自旋”)。在典型的现代体系结构上,这些操作生成的指令要求具有锁定内存位置的缓存线处于独占状态,这非常耗时(特别是当多个线程同时旋转时)。始终以加载/只读方式旋转,然后重试
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
}