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
}