Linux加油站 - 线程同步

本文介绍了多线程编程中的几种同步机制,包括互斥锁用于保护共享资源的访问,条件变量实现线程间的通信,信号量作为计数器控制资源访问,以及读写锁以优化读写操作的并发性能。通过具体的示例展示了这些机制如何在实际问题如卖票系统、生产者消费者模型中应用。
摘要由CSDN通过智能技术生成

目录

互斥锁

条件变量

信号量

读写锁


互斥锁

         互斥锁接口:

    pthread_mutex_t // 互斥量的类型 

    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
        - 功能:初始化互斥量
        - 参数 :
            - mutex : 需要初始化的互斥量变量
            - attr : 互斥量相关的属性,NULL(一般使用默认)

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
        - 释放互斥量的资源

    int pthread_mutex_lock(pthread_mutex_t *mutex);
        - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
        - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
        - 解锁

        案例描述,使用多线程实现用户买票问题: 有3个窗口同时卖票,一共是100张票,卖完即止。

int tickets = 100;   

void * mysellfunc(){

    while (tickets >0)
    {   
        usleep(6000);       
        printf("%ld 正在卖第 %d 张门票\n" , pthread_self() , tickets);
        tickets--;
    }
    return NULL; 
}

int main(){

    pthread_t tid1 , tid2 ,tid3;
    pthread_create(&tid1 , NULL , mysellfunc , NULL);
    pthread_create(&tid2 , NULL , mysellfunc , NULL);
    pthread_create(&tid3 , NULL , mysellfunc , NULL);

    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    pthread_exit(NULL);

    return 0;
}

         显然,没有进行线程同步的情况下,会出现非常混乱的情况:


        实现方面,显然,临界区(线程共享变量)即各线程对票数的售卖操作,因此对此操作需要加锁:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


int tickets = 1000;   
pthread_mutex_t mutex;     //创建互斥量类型

void * mysellfunc(void * arg){

    while (1)          
    {   
        pthread_mutex_lock(&mutex);
        
        if(tickets > 0)             
        {       
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n" , pthread_self() , tickets);
            tickets--;
        }
        else
        {   
            pthread_mutex_unlock(&mutex);
            break;
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(){

    pthread_mutex_init(&mutex , NULL);    // 初始化互斥量

    pthread_t tid1 , tid2 ,tid3;
    pthread_create(&tid1 , NULL , mysellfunc , NULL);
    pthread_create(&tid2 , NULL , mysellfunc , NULL);
    pthread_create(&tid3 , NULL , mysellfunc , NULL);

    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    pthread_exit(NULL);

    pthread_mutex_destroy(&mutex);     //  释放互斥量

    return 0;
}

         看看运行结果:


条件变量

        条件变量是配合互斥锁等去使用,因为锁所能达到的功能就只有加锁和解锁,并不能实现线程之间的一些关联,于是条件变量就出现了,与锁相互配合使用,达到线程同步。

        条件变量用于在线程之间进行通信,使得线程可以等待某个条件满足后再继续执行。条件变量必须与互斥锁一起使用,以确保线程在等待条件时不会访问共享资源。

        条件变量接口:

    pthread_cond_t     // 条件变量的类型 
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
        - 等待函数,调用了该函数,线程会阻塞在这里。
        - 注意一下,当运行到该函数并处于阻塞状态时,会先对持有的互斥锁进行解锁,从而不影响其他线程拿互斥锁。当不阻塞时(也就是被sinal函数唤醒后),会重新加锁,继续向下执行。

    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
        - 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
    int pthread_cond_signal(pthread_cond_t *cond);
        - 唤醒一个或者多个等待的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
        - 唤醒所有的等待的线程

       

        先看看什么是生产者消费者模型:

        上图表明生产者生产满了以后,要通知消费者去消费;同样的,消费者消费完后 ,也会通知生产者去生产, 如果需要实现以上的机制,就可以用到条件变量这种线程同步的机制。

        直接上实例:

        1、先创建五个生产者线程,五个消费者线程,并进行互斥锁和条件变量的初始化:

struct ListNode{
    int val;
    struct ListNode *next;
}*head;

head = NULL;

int main(){
    pthread_cond_init(&cond  , NULL);

    pthread_mutex_init(&mutex, NULL);

    //创建5个生产者线程和5个消费者线程
    pthread_t ptids[5] , ctids[5];
    for(int i = 0 ; i<5 ;i++)
    {
        pthread_create(&ptids[i] , NULL , producerfunc , NULL);
        pthread_create(&ctids[i] , NULL , customerfunc , NULL);

    }
    for(int i = 0 ; i<5 ;i++)
    {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }
    pthread_mutex_destroy(&mutex);

    pthread_cond_destroy(&cond);

    pthread_exit(NULL);

    return 0;
}

         2、以链表结点作为线程生产和消费的对象,

        生产者线程函数生产了一个结点就调用signal函数通知消费者线程进行消费(也就是删除结点):

void * producerfunc(void * arg){        
    while (1)    //生产者不断生产结点
    {      
        pthread_mutex_lock(&mutex);

        struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));         
        newNode ->next = head;
        head = newNode;
        newNode ->val = rand()%1000;
        printf("add Node, num: %d , tid  : %ld\n" , newNode->val , pthread_self());

        //只要生产了一个就通知 消费者消费(signal函数)
        pthread_cond_signal(&cond);
       
        pthread_mutex_unlock(&mutex);

        usleep(100);
    }
    return NULL;
}

        消费者线程函数:

void * customerfunc(void * arg){        //消费者不断的消费结点(删除)
    while (1)
    {
        pthread_mutex_lock(&mutex);

        struct ListNode *cur = head;
        if(head != NULL)            //判断是否有数据 (不等于空表示有数据,通知消费者去消费数据) 
        {   
            head = head ->next;
            printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self()); 
            free(cur);
            pthread_mutex_unlock(&mutex);

            usleep(100);
        }
        else
        {
            //没有数据需要等待(wait函数)
            pthread_cond_wait(&cond, &mutex);
            //该函数有两个参数,第二个参数即需要配合使用的锁,这个锁用来解开唤醒这个条件变量的所控制的线程。
            //补充一下,当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的时候,继续向下执行,会重新加锁。
            
            pthread_mutex_unlock(&mutex);
        }
    }
    return NULL;
}

          完整代码:

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

/创建条件变量
pthread_cond_t cond;


pthread_mutex_t mutex;

struct ListNode{
    int val;
    struct ListNode *next;
};

struct ListNode *head = NULL;

void * producerfunc(void * arg){        //生产者不断的生产结点
    while (1)
    {      
        pthread_mutex_lock(&mutex);

        struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));        
        newNode ->next = head;
        head = newNode;
        newNode ->val = rand()%1000;
        printf("add Node, num: %d , tid  : %ld\n" , newNode->val , pthread_self());

        /只要生产了一个就通知消费者 消费
        pthread_cond_signal(&cond);
        

        pthread_mutex_unlock(&mutex);

        usleep(100);
    }
    return NULL;
}

void * customerfunc(void * arg){        //消费者不断的消费结点(删除)
    
    while (1)
    {
        pthread_mutex_lock(&mutex);

        struct ListNode *cur = head;
        if(head != NULL)            //判断是否有数据 (不等于空表示有数据,通知消费者去消费数据) 
        {   
            head = head ->next;
            printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self()); 
            free(cur);
            pthread_mutex_unlock(&mutex);

            usleep(100);
        }
        else
        {
            / 没有数据需要等待
            pthread_cond_wait(&cond, &mutex);
             当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的时候,继续向下执行,会重新加锁。
            
            pthread_mutex_unlock(&mutex);

        }
    }
    return NULL;
}

int main(){
    //main函数初始化条件变量
    pthread_cond_init(&cond  , NULL);

    pthread_mutex_init(&mutex, NULL);

    //创建5个生产者线程和5个消费者线程
    pthread_t ptids[5] , ctids[5];
    for(int i = 0 ; i<5 ;i++)
    {
        pthread_create(&ptids[i] , NULL , producerfunc , NULL);
        pthread_create(&ctids[i] , NULL , customerfunc , NULL);
    }

    for(int i = 0 ; i<5 ;i++)
    {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    pthread_mutex_destroy(&mutex);
    
    //释放条件变量
    pthread_cond_destroy(&cond);
    
    pthread_exit(NULL);

    return 0;
}

         执行结果如下,可以看到,一个生产者线程生产了一个结点后,会通知一个消费者线程进行消费:


信号量

        信号量是一种计数器,用于控制对共享资源的访问。实现的目标与条件变量是相似的,都是用于阻塞线程的存在。主要不同的是,信号量有“信号灯”的概念,灯只有两种状态,要么是亮的,要么是灭的。这也就对应了二进制信号量的 0 ,1 两种状态。 灯亮了代表资源可用,灯灭了代表资源不可用。

        使用信号量其实只是限定了访问资源的次数。如果信号量里的值是0,则会阻塞进程,直到信号灯大于0为止。例如初始化信号灯的值是5,那么调用一次就会-1 ,为0时则会阻塞进程,不允许调用。

        来看看信号量的接口:

    信号量的类型 sem_t
    int sem_init(sem_t *sem, int pshared, unsigned int value);
        - 初始化信号量
        - 参数:
            - sem : 信号量变量的地址
            - pshared : 0 用在线程间 ,非0 用在进程间
            - value : 信号量中的值

    int sem_destroy(sem_t *sem);
        - 释放资源

    int sem_wait(sem_t *sem);
        - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞

    int sem_trywait(sem_t *sem);

    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

    int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

    int sem_getvalue(sem_t *sem, int *sval);

        使用案例:

        还是上面的生产者消费者模型样例,以下是简单的伪代码实现:

    sem_t psem;
    sem_t csem;  初始化信号量

    init(psem, 0, 8);
    init(csem, 0, 0);

    producer() {
        sem_wait(&psem);    会对8进行-1,变成7

        sem_post(&csem);     生产了一个内容要通知消费者去消费,因此要 +1        
    }

    customer() {
        sem_wait(&csem);
        sem_post(&psem)
    }

        

具体实现:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include<semaphore.h>

///     创建两个信号量
sem_t psem;
sem_t csem;
///


pthread_mutex_t mutex;
struct ListNode{
    int val;
    struct ListNode *next;
};
struct ListNode *head = NULL;

void * producerfunc(void * arg){        //生产者不断的生产结点
    
    while (1)
    {   
        ///   每次生产-1,可以知道在这个循环里生产了8个链表节点,生产者进来先消费一个信号灯
        sem_wait(&psem);        
        ///

        pthread_mutex_lock(&mutex);
        struct ListNode *newNode = (struct ListNode*)malloc(sizeof(struct ListNode));        
        newNode ->next = head;
        head = newNode;
        newNode ->val = rand()%1000;
        printf("add Node, num: %d , tid  : %ld\n" , newNode->val , pthread_self());
        pthread_mutex_unlock(&mutex);

        ///     当上面的减到0时候,证明发送给消费者的就是8了,表示他可以消费8个
        sem_post(&csem);
        ///

    }
    return NULL;
}

void * customerfunc(void * arg){        //消费者不断的消费结点(删除)
    
    while (1)
    {   
        ///   代码到这里 csem的值已经是8了,证明生产者给他生产了8个链表结点可以消费
        sem_wait(&csem);        
        ///

        pthread_mutex_lock(&mutex);
        struct ListNode *cur = head;
        head = head ->next;
        printf("delete node num: %d , tid: %ld\n", cur->val, pthread_self()); 
        free(cur);
        pthread_mutex_unlock(&mutex);

        ///     
        sem_post(&psem);
        ///

    }
    return NULL;
}


int main(){

    ///     初始化两个信号量
    sem_init(&psem , 0 , 8);
    sem_init(&csem , 0 , 0);
    ///

    pthread_mutex_init(&mutex, NULL);

    //创建5个生产者线程和5个消费者线程
    pthread_t ptids[5] , ctids[5];
    for(int i = 0 ; i<5 ;i++)
    {
        pthread_create(&ptids[i] , NULL , producerfunc , NULL);
        pthread_create(&ctids[i] , NULL , customerfunc , NULL);

    }

    for(int i = 0 ; i<5 ;i++)
    {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while (1)
    {
        sleep(10);
    }
    
    pthread_mutex_destroy(&mutex);
    
 
    pthread_exit(NULL);

    return 0;
    
}


读写锁

      考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

        在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现读写锁的特点:

  •         如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  •         如果有其它线程写数据,则其它线程都不允许读、写操作。
  •         写是独占的,写的优先级高。

        

        直接看案例,案例描述,8个线程操作同一个全局变量:3个线程不定时写这个全局变量,5个线程不定时的读这个全局变量。

        读写锁接口:

    pthread_rwlock_t//读写锁的类型 
       
    //初始化读写锁
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    
    //释放读写锁
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
    //解锁
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    
    //上读 OR 写锁
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

    

        1、先创建读写锁:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int nums = 0 ;    //全局变量

pthread_rwlock_t rwlock;

int main(){
    //  初始化读写锁
    pthread_rwlock_init(&rwlock , NULL);
    
    pthread_t wtids[3] , rtids[5];

    //创建读, 写线程
    for(int i = 0 ; i<3 ; i++)
    {
        pthread_create(&wtids[i] , NULL , myWrite , NULL);      //写线程池
    }

    for(int i = 0 ; i<5; i++)
    {
        pthread_create(&rtids[i] , NULL , myRead , NULL);       //读线程池
    }

    // 设置线程分离
    for(int i = 0 ; i<3 ; i++)
    {
        pthread_detach(wtids[i]);
    }

    for(int i = 0 ; i<5; i++)
    {
        pthread_detach(rtids[i]);
    }

    pthread_exit(NULL);

    // 释放读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

        2、初始化工作完毕后,开始编写读写线程的执行函数

        写:

void * myWrite(void *arg){
    while (1)
    {   
        // 写操作加写锁
        pthread_rwlock_wrlock(&rwlock);
        nums++;
        printf("++write , tid: %ld, nums: %d\n" , pthread_self() , nums);
        pthread_rwlock_unlock(&rwlock);

        usleep(100);
    }
    return NULL; 
}

        读:

void * myRead(void *arg){
    while (1)
    {   
        //读操作加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("==read , tid: %ld, nums: %d\n" , pthread_self() , nums);
        pthread_rwlock_unlock(&rwlock);

        usleep(100);
    }
    return NULL;
}

        整体代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int nums = 0 ;

pthread_rwlock_t rwlock;

void * myWrite(void *arg){
    while (1)
    {   
        // 写操作加写锁
        pthread_rwlock_wrlock(&rwlock);
        nums++;
        printf("++write , tid: %ld, nums: %d\n" , pthread_self() , nums);
        pthread_rwlock_unlock(&rwlock);

        usleep(100);
    }
    return NULL; 
}
void * myRead(void *arg){
    while (1)
    {   
        // 读操作加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("==read , tid: %ld, nums: %d\n" , pthread_self() , nums);
        pthread_rwlock_unlock(&rwlock);

        usleep(100);
    }
    return NULL;
}

int main(){
    //  初始化读写锁
    pthread_rwlock_init(&rwlock , NULL);
    
    pthread_t wtids[3] , rtids[5];

    // 创建读, 写线程
    for(int i = 0 ; i<3 ; i++)
    {
        pthread_create(&wtids[i] , NULL , myWrite , NULL);      //写线程池
    }

    for(int i = 0 ; i<5; i++)
    {
        pthread_create(&rtids[i] , NULL , myRead , NULL);       //读线程池
    }

    // 设置线程分离
    for(int i = 0 ; i<3 ; i++)
    {
        pthread_detach(wtids[i]);
    }

    for(int i = 0 ; i<5; i++)
    {
        pthread_detach(rtids[i]);
    }

    // 退出主线程
    pthread_exit(NULL);

    // 释放读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

        执行结果:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值