Linux C/C++ 开发(学习笔记四):多线程并发锁:互斥锁、自旋锁、原子操作、CAS

简介: Linux C/C++ 开发(学习笔记四):多线程并发锁:互斥锁、自旋锁、原子操作、CAS

一、多线程计数

背景:

火车抢票,总共10个窗口,每个窗口都同时进行10w张抢票

可以采用多线程的方式,火车票计数是公共的任务

#include<pthread.h>//posix线程 
#include<stdio.h>
#include<unistd.h>
#define THREAD_COUNT 10  //定义线程数10
//线程入口函数
void* thread_callback(void* arg){ 
    int* pcount=(int*)arg;
    int i=0;
    while(i++<100000){
        (*pcount)++;
        usleep(1);//单位微秒
    }
}
//10个窗口,同时对count进行++操作
int main(){
    pthread_t threadid[THREAD_COUNT]={0};//初始化线程id
    int count=0;
    for(int i=0;i<THREAD_COUNT;i++){//创建10个线程
        //第一个参数:返回线程。 第二个参数:线程的属性(堆栈)。第三个:线程的入口函数。第四个:主线程往子线程传的参数
        pthread_create(&threadid[i],NULL,thread_callback,&count);//count是传入thread_callback内的
    }
    for(int i=0;i<100;i++){
        printf("count: %d\n",count);
        sleep(1);//单位秒
    }
}

虽然包含了线程的头文件,可是编译的时候却报错“对pthread_create未定义的引用“,原来时因为 pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,在编译中要加-lpthread参数

可以使用下面方法编译

g++ thread_count.cpp -o thread_count -lpthread

执行后,发现,按理说要执行到100w,可是停到99w多就结束了。

二、发现问题

理想状态,线程应该是这样的

但实际上存在,执行完线程1MOV操作后,线程1切换到线程2。导致两个线程的操作,本应该50->52,但是结果确实50->51

count是一个临界资源(两个线程共享一个变量),因此为了避免上述这种情况发生,要加锁

三、互斥锁

当一个线程在执行一个指令的时候,另一个线程进不来。

相当于把count++转化为汇编的3行命令给打包在一起。

定义互斥锁

pthread_mutex_t mutex;//定义互斥锁

初始化互斥锁

pthread_mutex_init(&mutex,NULL);//互斥锁初始化(第二个参数是 锁的属性)

加锁/解锁

//加了互斥锁
 pthread_mutex_lock(&mutex);
 (*pcount)++;
 pthread_mutex_unlock(&mutex);

完整代码

#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#define THREAD_COUNT 10
pthread_mutex_t mutex;//定义互斥锁
void* thread_callback(void* arg){
    int* pcount=(int*)arg;
    int i=0;
    while(i++<100000){
#if 0
        (*pcount)++;
#else   
        //加了互斥锁
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
#endif
        usleep(1);//单位微秒
    }
}
//10个窗口,同时对count进行++操作
int main(){
    pthread_t threadid[THREAD_COUNT]={0};//初始化线程id
    pthread_mutex_init(&mutex,NULL);//互斥锁初始化(第二个参数是 锁的属性)
    int count=0;
    for(int i=0;i<THREAD_COUNT;i++){//创建10个线程
        //第一个参数:返回线程。 第二个参数:线程的属性(堆栈)。第三个:线程的入口函数。第四个:主线程往子线程传的参数
        pthread_create(&threadid[i],NULL,thread_callback,&count);//count是传入thread_callback内的
    }
    for(int i=0;i<100;i++){
        printf("count: %d\n",count);
        sleep(1);//单位秒
    }
}

四、自旋锁

在写法上和互斥锁基本上没有差别

定义自旋锁

pthread_spinlock_t spinlock;//定义自旋锁

初始化自旋锁

pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//自旋锁初始化(第二个参数是 进程共享)

自旋锁(加锁/解锁)

//加了自旋锁
pthread_spin_lock(&spinlock);
(*pcount)++;
pthread_spin_unlock(&spinlock);

完整代码

#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#define THREAD_COUNT 10
// pthread_mutex_t mutex;
pthread_spinlock_t spinlock;//定义自旋锁
void* thread_callback(void* arg){
    int* pcount=(int*)arg;
    int i=0;
    while(i++<100000){
#if 0
        (*pcount)++;
#elif 0 
        //加了互斥锁
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
#else 
        //加了自旋锁
        pthread_spin_lock(&spinlock);
        (*pcount)++;
        pthread_spin_unlock(&spinlock);
#endif
        usleep(1);
    }
}
int main(){
    pthread_t threadid[THREAD_COUNT]={0};
    // pthread_mutex_init(&mutex,NULL);
    pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//自旋锁初始化(第二个参数是 进程共享)
    int count=0;
    for(int i=0;i<THREAD_COUNT;i++){
        pthread_create(&threadid[i],NULL,thread_callback,&count);
    }
    for(int i=0;i<100;i++){
        printf("count: %d\n",count);
        sleep(1);//单位秒
    }
}

五、互斥锁和自旋锁的对比

自选锁的介绍

使用场景:

当锁的内容很少的时候,继续等待的时间代价比 线程切换的时间代价更小的 时候,选择使用自旋锁(因为互斥锁会切换线程,等待重新调度请求,判断锁是否被占用,如果占用,继续阻塞,并切换到其他线程。如果切换线程的代价比 等待的代价大,可以使用自旋锁。否则使用互斥锁)。

锁的内容比较多的时候,使用互斥锁。(比如,线程安全的红黑树,可以使用mutex)

也就是说:

锁的内容少/没有系统调用,等待的时间代价少-》用自旋锁

锁的内容多,等待时间代价大-》用互斥锁

六、原子操作

如果把这三条指令变成一条,那么就不会出现,这种问题了。

原子操作:单条cpu指令实现(因此使用范围有限,必须要是cpu指令中有的指令)

完整代码

#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#define THREAD_COUNT 10
// pthread_mutex_t mutex;
// pthread_spinlock_t spinlock;
//用一个函数去实现
int increase(int *value,int add){
    int old;
    __asm__ volatile(
        "lock;xaddl %2,%1;"
        :"=a"(old)
        :"m"(*value),"a"(add)
        :"cc","memory"
    );
    return old;
}
void* thread_callback(void* arg){
    int* pcount=(int*)arg;
    int i=0;
    while(i++<100000000){
#if 0
        (*pcount)++;
#elif 0 
        //加了互斥锁
        pthread_mutex_lock(&mutex);
        (*pcount)++;
        pthread_mutex_unlock(&mutex);
#elif 0 
        //加了自旋锁
        pthread_spin_lock(&spinlock);
        (*pcount)++;
        pthread_spin_unlock(&spinlock);
#else
    //原子操作
        increase(pcount,1);
#endif
        usleep(1);
    }
}
int main(){
    pthread_t threadid[THREAD_COUNT]={0};
    // pthread_mutex_init(&mutex,NULL);
    // pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//自旋锁初始化(第二个参数是 进程共享)
    int count=0;
    for(int i=0;i<THREAD_COUNT;i++){
        pthread_create(&threadid[i],NULL,thread_callback,&count);
    }
    for(int i=0;i<100;i++){
        printf("count: %d\n",count);
        sleep(1);//单位秒
    }
}

七、其他:CAS

CAS:compare and swap

cpu有这样一条指令cmpxchg(a,b,c),(其实就是原子操作的原理)。

它的意思是

if(a==b){
  a=c;
}

下面例子中,instance就是a,NULL是b,c是malloc(sizeof(object))

if(instance==NULL){
  instance=malloc(sizeof(object));
}

使用cas实现求和

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#define CAS(a_ptr, a_oldVal, a_newVal) __sync_bool_compare_and_swap(a_ptr, a_oldVal, a_newVal)
// #define AtomicAdd(a_ptr,a_count) __sync_fetch_and_add (a_ptr, a_count)
#define THREAD_COUNT 10
void* callback(void* arg){
    int* pcount=(int*)arg;
    for(int i=0;i<100000;i++){
        while(true){
            int current=(*pcount);
            if(CAS(pcount,current,current+1)) break;
        }
        usleep(1);
    }
    return (void*)nullptr;
}
int main(int argc,char** argv){
    pthread_t threadid[THREAD_COUNT]={0};
    int count=0;
    for(int i=0;i<THREAD_COUNT;i++){//创建10个线程
        pthread_create(&threadid[i],NULL,callback,&count);
    }
    for(int i=0;i<100;i++){
        printf("count: %d\n",count);
        sleep(1);//单位秒
    }
}


相关文章
|
7月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
422 1
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1209 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
263 6
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
485 12
|
11月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
262 0
|
11月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
411 0
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
242 16