C++多线程开发(二)线程同步

学习地址:大丙老师线程同步

1.线程同步的作用

(1)对临界资源的进程保护;

(2)对线程先后顺序的依次执行;

2.线程同步的方式-加锁

 

 

3.互斥锁

(1)互斥锁函数

    //在 Linux 中互斥锁的类型为 pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁.
    //在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。
    //一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,
    //被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
    pthread_mutex_t mutex_t;

    //初始化互斥锁
    // restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
    //int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    // 释放互斥锁资源            
    //int pthread_mutex_destroy(pthread_mutex_t *mutex);
    //参数:
    //mutex: 互斥锁变量的地址
    //attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
    pthread_mutex_init(&mutex_t,NULL);

    // 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
    //这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
    //没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
    //如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
    //当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞
    pthread_mutex_lock(&mutex_t);
    //调用这个函数对互斥锁变量加锁还是有两种情况:
    //如果这把锁没有被锁定是打开的,线程加锁成功
    //如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号
    pthread_mutex_trylock(&mutex_t);

    //code to do
    
    //对互斥锁解锁,不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
    pthread_mutex_unlock(&mutex_t);

 

(2)互斥锁的使用

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define MAX 100

// 全局变量
int number;

// 创建一把互斥锁,全局变量, 多个线程共享
pthread_mutex_t mutex;

// 线程处理函数
void* funcA_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);//A sleep so it gives up the CPU,and B catch the CPU,but mutex is locked,so B cannot work until A unlock the mutex
        number = cur;
        printf("Thread A, id = %lu, number = %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* funcB_num(void* arg)
{
    for(int i=0; i<MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        printf("Thread B, id = %lu, number = %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
        usleep(5);
    }

    return NULL;
}

int main(){
    pthread_t tid1,tid2;
    pthread_mutex_init(&mutex,NULL);
    //create sub thread
    pthread_create(&tid1,NULL,funcA_num,NULL);
    pthread_create(&tid2,NULL,funcB_num,NULL);
    //block and recycle the sources
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    //destory the mutex before exiting
    pthread_mutex_destroy(&mutex);
}

 

截取运行结果的部分,可以看到对number的操作得到的结果是递增不重复的,说明实现了同步

注意使用互斥锁的时候要避免死锁

4.读写锁

(1)读写锁函数

    //因为通过一把读写锁可以锁定读或者写操作,下面介绍一下关于读写锁的特点:
    //使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
    //使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
    //使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
    //如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。
    //读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。
    //读写锁是一把锁,锁的类型为 pthread_rwlock_t,有了类型之后就可以创建一把互斥锁了
    pthread_rwlock_t rwlock;
    //调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。
    pthread_rwlock_rdlock(&rwlock);
    //线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作
    pthread_rwlock_tryrdlock(&rwlock);
    //调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。
    pthread_rwlock_wrlock(&rwlock);
    //线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作
    pthread_rwlock_trywrlock(&rwlock);
    //锁的释放
    pthread_rwlock_unlock(&rwlock);

 

(2)读写锁实现线程同步

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define MAX 100

// 全局变量
int number;

//声明一个读写锁,全局变量
pthread_rwlock_t rwlock;

// 写的线程的处理函数
void* writeNum(void* arg)
{
    for(int i=0;i<MAX;i++)
    {
        pthread_rwlock_wrlock(&rwlock);
        int cur = number;
        cur ++;
        number = cur;
        printf("写操作完毕, number : %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        // 添加sleep目的是要看到多个线程交替工作
        usleep(rand() % 100);
    }

    return NULL;
}

// 读线程的处理函数
// 多个线程可以如果处理动作相同, 可以使用相同的处理函数
// 每个线程中的栈资源是独享
void* readNum(void* arg)
{
    for(int i=0;i<MAX;i++)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("读操作完毕,number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        usleep(rand() % 100);
    }
    return NULL;
}

int main(){
    pthread_t read[5],write[2];//5个读操作,2个写操作,读操作比较多,适合使用读写锁
    pthread_rwlock_init(&rwlock,NULL);
    //create read and write thread
    for (size_t i = 0; i < 5; i++)
    {
        pthread_create(&read[i],NULL,readNum,NULL);
    }
    for (size_t i = 0; i < 2; i++)
    {
        pthread_create(&write[i],NULL,writeNum,NULL);
    }
    
    //block and recycle the sources
    for (size_t i = 0; i < 5; i++)
    {
        pthread_join(read[i],NULL);
    }
    for (size_t i = 0; i < 2; i++)
    {
        pthread_join(write[i],NULL);
    }
    pthread_rwlock_destroy(&rwlock);
}

 

运行结果部分如下:

5.条件变量

(1)条件变量函数

    pthread_mutex_t mutex;
    //严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的
   //二者的区别如下:
//假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区 //条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。 //一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为 pthread_cond_t,这样就可以定义一个条件变量类型的变量了: //被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用。 pthread_cond_t cond; // 初始化 //int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); //cond: 条件变量的地址 //attr: 条件变量属性,一般使用默认属性,指定为 NULL pthread_cond_init(&cond,NULL); //通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情: //在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁 //当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区,而其他线程没有加锁成功会继续阻塞在这里,因此这里使用while循环进行判断 pthread_cond_wait(&cond,&mutex); // 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ }; // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了 //int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); //这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点: time_t mytim = time(NULL); // 1970.1.1 0:0:0 到当前的总秒数 struct timespec tmsp; tmsp.tv_nsec = 0; tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s pthread_cond_timedwait(&cond,&mutex,&tmsp); // 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞 //int pthread_cond_signal(pthread_cond_t *cond); pthread_cond_signal(&cond); // 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞 //int pthread_cond_broadcast(pthread_cond_t *cond); pthread_cond_broadcast(&cond); //调用上面两个函数中的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程 //区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定) //pthread_cond_broadcast 是唤醒所有被阻塞的线程。 pthread_cond_destroy(&cond);

(2)条件变量的使用

/*
生产者线程 -> 5个
生产商品或者任务放入到任务队列中
任务队列满了就阻塞,不满的时候就工作
通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
消费者线程 -> 5个
读任务队列,将任务或者数据取出
任务队列中有数据就消费,没有数据就阻塞
通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
*/
#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<vector>
#include<stdlib.h>

struct Node
{
    int number;
    Node* next;
};

struct Node* head;//声明一个链表,用于保存生产出来的待消费的产品
pthread_mutex_t mutex;//互斥锁,用于对产品链表进行互斥资源保护(同步)
pthread_cond_t cond;//条件变量

//生产者处理函数
void* product(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        Node* newNode=(Node*)malloc(sizeof(Node));
        newNode->number=rand()%1000;
        newNode->next=head;
        head=newNode;
        printf("product,tid:%ld,number:%d\n",pthread_self(),newNode->number);
        pthread_mutex_unlock(&mutex);
        pthread_cond_broadcast(&cond);//生产出产品,因此唤醒条件变量阻塞的线程
        sleep(rand()%3);
    }
    return NULL;
}

//消费者处理函数
void* consume(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        //这里必须使用while循环判断而不能使用if循环判断。
        //如果使用if循环判断,那么就是在条件变量cond满足的时候释放了cond,程序继续向下执行而不会再次进行链表判空。
     //根据生产者的执行可知当生产出一个产品之后会唤醒所有阻塞在此的线程,因此可能此线程获得cpu资源的时候,链表已经又被取空了,故而需要进行while再做一次判断保证链表非空。
while(head==NULL) { pthread_cond_wait(&cond,&mutex);//条件变量,如果没有释放那么cond会阻塞在这里,而cond的阻塞会释放互斥锁mutex,使得生产者可以获得产品链表的锁从而能够进行生产操作。同时其他消费者也可以竞争此锁,不过由于链表为空,因此同样也会被条件变量阻塞在此。 } Node* root=head; head=head->next; printf("consume,tid:%ld,number:%d\n",pthread_self(),root->number); free(root); pthread_mutex_unlock(&mutex); sleep(rand()%3); } return NULL; } int main() { //init pthread_cond_mutex pthread_cond_init(&cond,NULL); pthread_mutex_init(&mutex,NULL); //init pthread pthread_t producer[5]; pthread_t consumer[5]; for(int i=0;i<5;i++) { pthread_create(&producer[i],NULL,product,NULL); pthread_create(&consumer[i],NULL,consume,NULL); } for(int i=0;i<5;i++) { pthread_join(producer[i],NULL); pthread_join(consumer[i],NULL); } //destory thread pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }

6.信号量

(1)信号量函数

    //信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
    //信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
    //信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t 对应的头文件为 <semaphore.h>:
    sem_t sem;
    // 初始化信号量/信号灯
    //int sem_init(sem_t *sem, int pshared, unsigned int value);
    //sem:信号量变量地址
    //pshared:0:线程同步 非 0:进程同步
    //value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
    sem_init(&sem,0,5);
    sem_destroy(&sem);

    // 参数 sem 就是 sem_init() 的第一个参数  
    // 函数被调用sem中的资源就会被消耗1个, 资源数-1
    //当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。
    sem_wait(&sem);
    //线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
    sem_trywait(&sem);

    // 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
    //struct timespec {
        //time_t tv_sec;      /* Seconds */
        //long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
    //};
    // 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
    // abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
    //int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    //当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。
    timespec spec;
    time_t now=time(NULL);
    spec.tv_nsec=0;
    spec.tv_sec=now+100;
    sem_timedwait(&sem,&spec);

    //调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
    sem_post(&sem);

    //通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。
    int semvalue;
    sem_getvalue(&sem,&semvalue);

 

(2)利用信号量机制处理生产者消费者

/*
生产者线程 -> 5个
生产商品或者任务放入到任务队列中
任务队列满了就阻塞,不满的时候就工作
通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
消费者线程 -> 5个
读任务队列,将任务或者数据取出
任务队列中有数据就消费,没有数据就阻塞
通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
*/
#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<vector>
#include<stdlib.h>
#include<semaphore.h>
#include<time.h>

struct Node
{
    int number;
    Node* next;
};

struct Node* head;
pthread_mutex_t mutex;//互斥锁
sem_t empty_sem;//空闲区个数信号量
sem_t full_sem;//装填区个数信号量

void* product(void* arg)
{
    while(1)
    {
        sem_wait(&empty_sem);//信号量锁需要在互斥锁之前,因为如果顺序颠倒可能会导致死锁->颠倒之后的逻辑:将链表加锁,然后因为资源不满足被阻塞在sem_wait,但是由于链表被锁了其他进程也无法操作,故而一直死锁在这里
        pthread_mutex_lock(&mutex);//由于资源个数不止一个所以可以有多个线程工作,故而对于链表的临界资源需要进行互斥锁保护
        Node* newNode=(Node*)malloc(sizeof(Node));
        newNode->number=rand()%1000;
        newNode->next=head;
        head=newNode;
        printf("product,tid:%ld,number:%d\n",pthread_self(),newNode->number);
        pthread_mutex_unlock(&mutex);
        sem_post(&full_sem);
        sleep(rand()%3);
    }
    return NULL;
}

void* consume(void* arg)
{
    while(1)
    {
        sem_wait(&full_sem);
        pthread_mutex_lock(&mutex);        Node* root=head;
        head=head->next;
        printf("consume,tid:%ld,number:%d\n",pthread_self(),root->number);
        free(root);
        pthread_mutex_unlock(&mutex);
        sem_post(&empty_sem);
        sleep(rand()%3);
    }
    return NULL;
}


int main()
{
    //init
    pthread_mutex_init(&mutex,NULL);
    sem_init(&full_sem,0,0);
    sem_init(&empty_sem,0,5);
    //init thread
    pthread_t producer[5];
    pthread_t consumer[5];
    for(int i=0;i<5;i++){
        pthread_create(&producer[i],NULL,product,NULL);
        pthread_create(&consumer[i],NULL,consume,NULL);
    }
    //recycle sources
    for(int i=0;i<5;i++){
        pthread_join(producer[i],NULL);
        pthread_join(consumer[i],NULL);
    }
    //destory
    pthread_mutex_destroy(&mutex);
    sem_destroy(&full_sem);
    sem_destroy(&empty_sem);
    return 0;
}

 

 部分运行结果:

 

posted on 2021-09-20 11:05  freden  阅读(594)  评论(0编辑  收藏  举报