互斥锁、条件变量、信号量以及适用场景

文章目录

    • 互斥锁
      • 互斥锁实战过程中常用方法
    • 条件变量
      • 条件变量实战过程中常用方法
    • 信号量
      • 信号量的常用方法
    • 生产者和消费者问题
      • 一个粗略版本的生产者消费者代码(如果只使用了互斥锁)
      • 一个改进版本的生产者消费者代码(使用了互斥锁和条件变量)
      • 一个最终版本的生产者消费者代码(使用了互斥锁和信号量)
    • 总结

互斥锁

互斥锁主要是用在多线程编程时,多个线程同时访问同一个变量的情况下,保证在某个时刻只能有一个线程访问。每个线程在访问共享变量的时候,首先要先获得锁,然后才能访问共享变量,当一个线程成功获得锁时,其他变量都会阻塞在获取锁这一步,直到这个线程释放掉锁。

互斥锁实战过程中常用方法

pthread_mutex_t mutex;//创建一个互斥量
pthread_mutex_init(&mutex,NULL); //初始化锁
pthread_mutex_lock(&mutex); //在需要上锁的片段加锁
pthread_mutex_unlock(&mutex); //在需要去锁的片段去锁
pthread_mutex_destroy(&mutex);//锁用完之后销毁锁

条件变量

条件变量的作用是用于多线程之间的线程同步。线程同步是指线程间需要按照预定的先后顺序进行的行为,比如我想要线程1完成了某个步骤之后,才允许线程2开始工作,这个时候就可以使用条件变量来达到目的。

条件变量实战过程中常用方法

pthread_cond_t cond;//创建条件变量
pthread_cond_init(&cond,NULL);//初始化条件变量
pthread_cond_signal(&cond);//通常搭配wait函数用,向条件变量发送唤醒信号
pthread_cond_wait(&cond,&mutex);//先阻塞,直至接收到signal或者broadcast后被唤醒
pthread_cond_destroy(&cond);//使用完之后销毁条件变量

信号量

信号量可以用于解决多线程环境中的竞争条件和协调线程的执行顺序

信号量的常用方法

int sem_init(sem_t *sem,int pshared,unsigned int value);//作用:初始化信号量 参数 pshared:0 用在线程间 非0 用在进程间
int sem_wait(sem_t *sem);//对信号量的值减1,如果值为0 就阻塞
int sem_post(sem_t *sem);//对信号量解锁,调用一次对信号量的值+1
int sem_destroy(sem_t *sem);//释放资源

生产者和消费者问题

生产者(Producers):生产者进程或线程的任务是生成某种类型的数据或物品,并将其放入共享缓冲区。生产者可能会生成多个数据项,并且在放置数据到缓冲区时需要确保缓冲区未满。
消费者(Consumers):消费者进程或线程的任务是从共享缓冲区中取出数据项并进行处理或消耗。消费者可能会以一定的速度获取数据,需要确保缓冲区不为空。
共享缓冲区(Buffer):这是生产者和消费者之间的共享数据结构,用于存储生产者生成的数据项。缓冲区的容量是有限的,生产者在缓冲区已满时需要等待,消费者在缓冲区为空时需要等待。

一个粗略版本的生产者消费者代码(如果只使用了互斥锁)

//生产者部分代码
void * producer(void * arg){
    //生产者不断的生产
    //不断的创建新的结点
    while(1){
        pthread_mutex_lock(&mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand()%1000;
        printf("add node,num:%d,tid:%ld\n",newNode->num,pthread_self());
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    
    return NULL;
}
void * customer(void * arg){
    //消费者不断的消费数据
    while(1){
        pthread_mutex_lock(&mutex);
        if(head){
            //如果有数据
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\n",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);    
            usleep(100);            
        }else{
            pthread_mutex_unlock(&mutex);
           
        }               
    }
    return NULL;
}

虽然用了互斥锁,实现了生产者的若干个线程和消费者若干个线程互斥访问,但是如果消费者线程抢到锁之后没有资源可供消耗的话,会空转消耗cpu,抢占到锁没有用

一个改进版本的生产者消费者代码(使用了互斥锁和条件变量)

void * producer(void * arg){
    //生产者不断的生产
    //不断的创建新的结点
    while(1){
        pthread_mutex_lock(&mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand()%1000;
        printf("add node,num:%d,tid:%ld\n",newNode->num,pthread_self());
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    
    return NULL;
}
void * customer(void * arg){
    //消费者不断的消费数据
    while(1){
        pthread_mutex_lock(&mutex);
        if(head){
            //如果有数据
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\n",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);    
            usleep(100);            
        }else{
            //如果没有数据,需要等待,阻塞了,直到生产者执行了pthread_cond_signal(&cond)才会唤醒      
            pthread_cond_wait(&cond,&mutex);
            //这个函数起两个作用,
            //1.是先释放mutex让执行其他代码
            //2.是直到pthread_cond_signal 或 pthread_cond_broadcast执行后唤醒该函数同时重新加锁
            //阻塞解除后要释放锁
            pthread_mutex_unlock(&mutex);  
        }
    }
    return NULL;
}

上述方法已经解决了在生产者还未生产资源的时候,cpu空转的情况。在代码中,使用了条件变量,当未生产资源时候,消费者会阻塞在pthread_cond_wait(&cond,&mutex);当生产者调用pthread_cond_signal(&cond)才会唤醒消费者继续执行,不会跟生产者抢占锁!
但是未考虑容量有限的情况,也就是说生产者能一直生产

一个最终版本的生产者消费者代码(使用了互斥锁和信号量)

void * producer(void * arg){
    //生产者不断的生产
    //不断的创建新的结点
    while(1){
        sem_wait(&psem);//调用一次先判断是否为0 为0阻塞 不为0值减1 
        //不能放到锁里面 假如放到锁里面 抢到锁之后,阻塞到wait,就永远死锁了
        //wait可以通过另一个线程的post解除锁
        //加互斥锁之后只能通过自身的代码解除
        pthread_mutex_lock(&mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand()%1000;
        printf("add node,num:%d,tid:%ld\n",newNode->num,pthread_self());

        pthread_mutex_unlock(&mutex);
        sem_post(&csem);//表示消费者有一个可以去消费
        usleep(100);
    }    
    return NULL;
}
void * customer(void * arg){
    //消费者不断的消费数据
    while(1){
            sem_wait(&csem);//先判断是否为0 为0阻塞 不为0消耗一个csem
            pthread_mutex_lock(&mutex);
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\n",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);   
            sem_post(&psem); //增加一个生产的位置
            usleep(100);                    
    }
    return NULL;
}

最终版本通过信号量实现了线程间同步,同时也可以做到有限的缓冲区容量,并且不会死锁,必须将信号量放置在互斥锁之前!!!

总结

互斥锁实现线程间互斥访问某一片段,实现了线程的互斥访问,同一时刻只能一个拿锁!
条件变量通常搭配互斥锁使用,目的是实现线程间的按一定顺序访问,即线程同步。
信号量通过消费者和生产者持有资源的变化动态调整。当生产一个产品,意味着多了一个可消耗的产品。当消耗一个产品,意味着可以再生产一个产品。

你可能感兴趣的:(C++,高并发服务器开发,线程同步,操作系统,c++)