众所周知,多线程环境下如果不使用“锁”来对非原子数据进行操作的话,将会导致数据错误,轻则数值错误,重则公司破产(秒杀系统超卖)
在C++中,有两种锁,一种是MUTEX,互斥锁,即当某个线程调用锁时,其他线程将会阻塞等待那个线程释放锁,是最简单的并发控制,另一种则是用atomic类型变量实现的cas自旋锁,只需要耗费一点CPU时间即可完成快速的加锁解锁,具体的性能差距我们看看代码和数据就知道了。
SPIN自旋锁则是使用CAS实现的,编程方式能够更加灵活
spin自旋锁好处是锁的等待时间短,坏处是当业务代码耗时太多时会空转CPU,导致越来越慢,可以适当usleep,这里我也会做一个自旋锁usleep时长的测试
我稍微实现了一个并发的list。控制加锁方式是在第六行的define,LCK为CAS实现的自选锁,MTX为C++标准库的mutex互斥锁,SPIN为cas实现的自旋锁。计时则是利用linux中的time命令来计时,编译带上-lpthread即可
spin.h 【自旋锁文件】
#include <unistd.h>
class SpinLock {
public:
SpinLock() : flag_(false)
{}
void lock()
{
bool expect = false;
while (!flag_.compare_exchange_weak(expect, true))
{
//这里一定要将expect复原,执行失败时expect结果是未定的
expect = false;
//这里我会分别测试0,10,100,1000的结果
usleep(10);
}
}
void unlock()
{
flag_.store(false);
}
private:
std::atomic<bool> flag_;
};
main文件
#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>
#include <unistd.h>
#include <cstdlib>
#include "spin.h"
#define SPIN //LCK:无锁方式插入队列 MTX:互斥锁方式插入队列 RAW_CAS:使用c语言bool __sync_bool_compare_and_swap函数实现的lock-free
using namespace std;
mutex mtx;
SpinLock sl;
const int th_nums=50; //线程个数
const int insert_num=500000; //每个线程插入的个数
atomic<int> done_nums(0);
struct node
{ //节点模型
int val;
node *next;
node(int n) : val(n), next(nullptr)
{
}
};
atomic<node*> head;
void push(int val){
node *new_obj=new node(val);
#ifdef MTX
mtx.lock();
new_obj->next=head;
head=new_obj;
mtx.unlock();
#endif
#ifdef LCK
new_obj->next=head;
while(!head.compare_exchange_weak(new_obj->next,new_obj)){ //当head在被其他线程所改变而不能CAS时
new_obj->next=head;
}
#endif
#ifdef SPIN
sl.lock();
if(head==nullptr){
head.store(new_obj);
sl.unlock();
return;
}
new_obj->next=head;
head.store(new_obj);
sl.unlock();
#endif
}
void func(int i) //线程调用函数,i号线程插入insert_num个数字到队列中
{
for (int j = 0; j < insert_num; j++)
{
push(i);
}
done_nums++; //某个线程完成任务后,这个atomic_int自增,main会判断如果所有线程都完成则结束程序
}
int main()
{
head=(nullptr);
for(int i=0;i<th_nums;i++){
move(thread(func,i)).detach();
}
while(done_nums.load()!=th_nums){ //等待所有线程完成
usleep(1);
}
//检查结果是否正确,我们遍历这个链表,其总的个数应该是 线程数*每个线程插入的个数
int total=0;
for(auto it=head.load();it!=nullptr;it=it->next){
//cout<<it->val<<" ";
total++;
}
if(total==th_nums*insert_num){
cout<<"ok";
}else{
cout<<"err";
}
return 0;
}
每个锁我都在50*50W的数据下跑了5轮测试
测试数量:50线程*50W数据 | |||||
测试命令:time -cq | |||||
MTX互斥锁 | |||||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 5.073 | 5.12 | 5.151 | 5.217 | 5.232 |
user | 10.311 | 9.957 | 10.277 | 10.438 | 10.008 |
sys | 8.323 | 8.33 | 8.841 | 8.896 | 9.372 |
CAS | |||||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 2.696 | 2.979 | 3.041 | 2.956 | 2.784 |
user | 8.645 | 8.652 | 8.485 | 8.385 | 8.391 |
sys | 0.794 | 1.178 | 1.616 | 1.869 | 0.993 |
SPIN | usleep | 0ms | |||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 67.56 | ||||
user | 339.524 | ||||
sys | 5.487 | ||||
SPIN | usleep | 10ms | |||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 4.857 | 4.994 | 5.207 | 5.183 | 5.349 |
user | 8.404 | 8.87 | 9.149 | 9.087 | 9.243 |
sys | 4.729 | 4.885 | 5.237 | 5.381 | 5.942 |
SPIN | usleep | 100ms | |||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 5.618 | 5.602 | 5.671 | 5.634 | 5.577 |
user | 7.618 | 7.465 | 7.496 | 7.688 | 7.281 |
sys | 3.072 | 3.22 | 3.271 | 3.037 | 3.273 |
SPIN | usleep | 1000ms | |||
epoch | 1 | 2 | 3 | 4 | 5 |
real | 7.736 | 7.695 | 7.649 | 7.634 | 7.531 |
user | 8.407 | 8.425 | 8.366 | 8.227 | 7.921 |
sys | 1.938 | 1.845 | 1.857 | 1.963 | 2.048 |
基本上,user
时间是程序在CPU上运行的时间,sys
时间是程序等待操作系统执行任务的时间。如果您对基准测试感兴趣,user + sys
是一个使用的好时机。 real
可能会受到其他正在运行的进程的影响,并且更加不一致
可以看出sys的时间大幅度降低,可以理解为因为使用了CAS自选锁而不是mutex互斥锁,thread之间相互等待的时间变短了
但是当我们使用spin时,如果获取锁失败后不休眠马上循环获取新锁,就会导致CPU占用400%,申请内存的业务代码很难跑起来,50*50W的数据跑了5分钟,这个就不测试了,只有一组数据,感兴趣的读者可以将usleep去掉看看速度
最后将每一种锁的sys,real,user时间平均可以得到下面的数据【spin的0ms就不放了,300多s没有比较的意义】
AVE | MTX互斥锁 | CAS | SPIN 10ms | SPIN 100ms | SPIN 1000ms |
real | 5.1586 | 2.8912 | 5.118 | 5.6204 | 7.649 |
user | 10.1982 | 8.5116 | 8.9506 | 7.5096 | 8.2692 |
sys | 8.7524 | 1.29 | 5.2348 | 3.1746 | 1.9302 |
real时间是整个系统自然流失的时间,可以理解为 你从运行./可执行文件到显示结果所自然执行的时间,越短越好,当我们的spin休眠时间越长,real越长,表示大部分线程等待时间变长了
time | MTX互斥锁 | CAS | SPIN 10ms | SPIN 100ms | SPIN 1000ms |
sys+user | 18.9506 | 9.8016 | 14.1854 | 10.6842 | 10.1994 |
可以看出在锁的竞争比较激烈的时候,MTX表现出了最差的性能
spin在不休眠的情况下情况最差
CAS编写的无锁原生(即不用SPIN锁保护临界区而是之间CAS指针)list性能最好,但是通用性较差
CAS实现的spin锁通用性较好,在对业务代码运行时间和锁竞争时间估计正确的情况下,能够达到和原生CAS差不多的性能以及更广的适用性(例如用他来保护一些更加复杂的操作而不仅仅是list的head指针)