7.线程同步-互斥锁、自旋锁、读写锁

297 阅读6分钟

线程安全,一切麻烦从共享资源开始

多个线程访问共享资源(全局和静态)的时候会冲突
三个概念:原子性、可见性和顺序性

原子性:
    一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)
    CPU执行指令:读取指令、读取内存、执行指令、写回内存
    i++ 1)从内存中读取i的值; 2)把i+1; 3)把结果写回内存
可见性:
    当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到
    cpu有调整缓存。每个线程读取共享变量时,会将变量从内存加载到cpu的缓存中,修改该变量后,cpu会立即更
    新缓存,但不一定会立即将它写回内存。此时其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程
    更新后的数据
顺序性:
    程序执行的顺序按照代码的先后顺序执行
    cpu为了提高程序整体的执行效率,可能会对代码进行优化,按照更高效的顺序执行代码
    cpu虽然并不保证完全按照代码顺序执行,但它保证程序最终的执行结果与代码顺序执行的结果一致。

如何解决线程安全问题
    volatile关键字(解决不了多线程修改同一变量的问题)
        保证了变量的内存可见性,cpu每次从内存中读取,不缓存
        禁止代码重排序。
        因为不是原子的

    原子操作
        本质是总线锁
        三条汇编指令:xadd、cmpxchg或xchg
        硬件级别的锁
    线程同步        

线程同步,各种锁的使用

互斥锁
    只有加锁和解锁操作,确保同一时间只有一个线程访问共享资源
    访问共享资源之前加锁,访问完成后释放锁
    如果某线程持有锁,其它的线程形成等待队列
    属性有:PTHREAD_MUTEX_TIMED_NP缺省值,普通锁,保证资源分配的公平性
    PTHREAD_MUTEX_RECURSIVE_NP嵌套锁,允许同一个线程对同一个锁成功获得多次
    PTHREAD_MUTEX_ADAPTIVE_NP适应锁,解锁后,请求锁的线程重新竞争
    
自旋锁
    互斥锁在等待锁的时候,线程会休眠不消耗CPU,而自旋锁则有一个循环不断地检查锁是否可以使用,消耗CPU
    两者没有好坏之分,只是场景不一样。自旋锁适用于等待很短的使用场景,而互斥锁适用于可能等待比较长的场景
    所以自旋锁没有提供超时的函数,因为使用此锁是基于等待很短的使用场景,否则用互斥锁

读写锁
    读写锁允许更高的并发性
    三种状态:读模式加锁(读锁),写模式加锁(写锁),不加锁
    特点:
        只要没有线程持有写锁,任意线程都可以成功申请读锁
        只有在不加锁状态时,才能成功申请写锁
    注意事项:
        读定你起码适合于对读的次数远大于写的情况
        linux系统优先考虑读锁,这种实现方式有可能导致写入线程饿死的情况
        pthread_rwlock_t mutex  //声明锁
        int pthread_rwlock_init() //初始化锁
        int pthread_rwlock_destroy();   
        pthread_rwlock_t rwlock = PTHREAD_RWLOCK_IINITIALIZER
        int pthread_rwlock_rdlock()  //申请读锁
        int pthread_rwlock_tryrdlock() //尝试申请读锁,不阻塞
        int pthread_rwlock_timedrdlock() //申请读锁,再超时机制
条件变量
    与互斥锁一起使用
    实现生产消费者模型
    实现通知的功能

信号量
    一个整数计数器,其数值用于表示空闲临界资源的数量
    申请资源时,信号量减少,表示可用资源数减少
    释放资源时,信号量增加,表示可用资源数增加

生产消费者模型
    1.条件变量+互斥锁实现生产消费者模型
    pthread_cond_wait(&cond, &mutex)
    把互斥锁解锁-->阻塞,等待条件(被唤醒)-->条件被触发+给互斥锁加锁
    
    2.信号量实现生产消费者模型
    

互斥锁

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>

using namespace std;

int var=0;

//线程同步-互斥锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void *thmain(void *arg);    //线程主函数

int main()
{
    //pthread_mutex_init(&mutex, NULL); //初始化互斥量 PTHREAD_MUTEX_INITIALIZER有这个则不需要调用此方法初始化

    pthread_t thid1=0, thid2=0;
    if(pthread_create(&thid1, NULL, thmain, NULL) != 0) {printf("thid1 faile\n"); exit(-1);}
    if(pthread_create(&thid2, NULL, thmain, NULL) != 0) {printf("thid2 faile\n"); exit(-1);}

    //等待子线程退出
    printf("join...\n");
    pthread_join(thid1, NULL);
    pthread_join(thid2, NULL);
    printf("join ok.\n");

    printf("var=%d\n", var);

    pthread_mutex_destroy(&mutex);      //主进程退出的时候销毁互斥量
    return 0;
}

void *thmain( void *arg)
{
    for(int i=0; i <100000; i++)
    {
        pthread_mutex_lock(&mutex);
        var++;
        pthread_mutex_unlock(&mutex);
        //__sync_fetch_and_add(&var,1);   //原子操作
        //printf("pthread=%d\n", pthread_self());
    }
}

自旋锁

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>

using namespace std;

int var=0;

//线程同步-自旋锁
pthread_spinlock_t spin;

void *thmain(void *arg);    //线程主函数

int main()
{
    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); 

    pthread_t thid1=0, thid2=0;
    if(pthread_create(&thid1, NULL, thmain, NULL) != 0) {printf("thid1 faile\n"); exit(-1);}
    if(pthread_create(&thid2, NULL, thmain, NULL) != 0) {printf("thid2 faile\n"); exit(-1);}

    //等待子线程退出
    printf("join...\n");
    pthread_join(thid1, NULL);
    pthread_join(thid2, NULL);
    printf("join ok.\n");

    printf("var=%d\n", var);

    pthread_spin_destroy(&spin);      //主进程退出的时候销毁自旋锁
    return 0;
}

void *thmain( void *arg)
{
    for(int i=0; i <100000; i++)
    {
        pthread_spin_lock(&spin);
        var++;
        pthread_spin_unlock(&spin);
        //__sync_fetch_and_add(&var,1);   //原子操作
        //printf("pthread=%d\n", pthread_self());
    }
}

读写锁

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>
#include <signal.h>

using namespace std;

/*
        读写锁允许更高的并发性
        三种状态:读模式加锁(读锁),写模式加锁(写锁),不加锁
        特点:
            只要没有线程持有写锁,任意线程都可以成功申请读锁
            只有在不加锁状态时,才能成功申请写锁
        注意事项:
            读定你起码适合于对读的次数远大于写的情况
            linux系统优先考虑读锁,这种实现方式有可能导致写入线程饿死的情况
*/
int var=0;
//线程同步-读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //声明读写锁并初始化

void *thmain(void *arg);    //线程主函数

void handle(int sig);       //信号15的处理函数

int main()
{
    signal(15, handle);     //设置信号15的处理函数

    pthread_t thid1=0, thid2=0, thid3=0;
    if(pthread_create(&thid1, NULL, thmain, NULL) != 0) {printf("thid1 faile\n"); exit(-1);}
    if(pthread_create(&thid2, NULL, thmain, NULL) != 0) {printf("thid2 faile\n"); exit(-1);}
    if(pthread_create(&thid3, NULL, thmain, NULL) != 0) {printf("thid3 faile\n"); exit(-1);}

    //等待子线程退出
    printf("join...\n");
    pthread_join(thid1, NULL);
    pthread_join(thid2, NULL);
    pthread_join(thid3, NULL);
    printf("join ok.\n");

    pthread_rwlock_destroy(&rwlock);      //主进程退出的时候销毁锁
    return 0;
}

void *thmain( void *arg)
{
    for(int i=0; i <1000; i++)
    {
        printf("线程%lu开始申请读锁...\n", pthread_self());
        pthread_rwlock_rdlock(&rwlock);         //加锁
        printf("线程%lu开始申请读锁成功\n", pthread_self());        //3个线程都可以申请到读锁,
        sleep(5);
        pthread_rwlock_unlock(&rwlock); 
        printf("线程%lu己释放读锁。\n", pthread_self());
        
        if( i == 4)
        {        
            //sleep(5);       //如果这里不sleep,3个线程则一直占着读锁,那写锁一直申请不成功
            //只有在不加锁状态时,才能成功申请写锁
        }
    }
}

void handle(int sig)
{
    printf("开始申请写锁...\n");
    pthread_rwlock_wrlock(&rwlock);     //加锁
    printf("申请写锁成功.\n");
    sleep(10);
    pthread_rwlock_unlock(&rwlock);     //有写锁,如果这里不解锁,则上面就无法再申请到读锁
}