C++ 的并发学习:CAS,SPIN和MUTEX

11 篇文章 0 订阅

众所周知,多线程环境下如果不使用“锁”来对非原子数据进行操作的话,将会导致数据错误,轻则数值错误,重则公司破产(秒杀系统超卖)

在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指针) 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值