原子操作是我们常常听到的一个概念,合理使用原子操作可以显著降低因为锁同步耗费时间。文章首先将会讨论以下问题:
- 为什么需要互斥锁
- 互斥锁的成本
- 如何用原子操作替代互斥锁
一、为什么需要互斥锁?
主要为了解决多个线程并发造成的数据争抢导致的脏数据。下面的程序创建了十个线程,线程执行的内容都是将全局变量count自加1一万次,按照正常逻辑,最终的count结果应该是:10W。
#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#include <atomic>
#include <chrono>
#include <mutex>
using namespace std::chrono_literals;
int count;
std::mutex lo;
void addOne()
{
for(int i=0;i<10000;i++)
{
count++;
}
}
int main()
{
auto s=std::chrono::steady_clock::now();
std::vector<std::thread * > pths;
std::generate_n(std::back_inserter(pths),10,[]{return new std::thread(addOne);});
std::for_each(pths.begin(),pths.end(),[](std::thread *ele){ele->join();});
auto e=std::chrono::steady_clock::now();
std::chrono::duration<double,std::milli> d=e-s;
std::cout<<count<<std::endl;
std::cout<<d.count()<<"ms"<<std::endl;
return 0;
}
输出结果如下:
42414 0.489784ms
结果表明,数据没有被正确的加到10W。这是为什么?一个线程正在进行自增操作时,另一个线程打断了这个操作,导致最开始的线程再也没有办法完成这个操作了,我们说这两个线程进行了数据争抢,争抢的结果是打断无法正确增加,打断多少次就会少多少次自增操作。对修改数据代码部分进行加锁可以避免这种情况。
二、互斥锁的成本
修改方式也比较简单,只需要在count前后加上lock和unlock。
std::mutex lo;
void addOne()
{
for(int i=0;i<10000;i++)
{
lo.lock();
count++;
lo.unlock();
}
}
结果如下:
100000 12.6528ms
现在count被正确增加到了10W,但是时间却从之前(未加锁前)的0.46ms增加到了12.54ms,增加了26.26倍,问题解决了,但是时间却成倍增加。这时候就要到我们的主角出场了———原子操作。
三、原子操作实现lock-free
原子操作头文件:
#include <atomic>
然后将count声明为atomic_int:
atomic_int count(0);
代码如下:
std::atomic_int count(0);
std::mutex lo;
void addOne()
{
for(int i=0;i<10000;i++)
{
count++;
}
}
100000 1.93808ms
数据被正确的增加到10W,时间比什么都不做的只增加了54%的时间。
【1】https://zhuanlan.zhihu.com/p/340359732