多线程同步与互斥

版权声明:允许转载,请注明文章出处 https://blog.csdn.net/Vickers_xiaowei/article/details/84891592

多线程线程基础操作
关于本篇博客的更多代码:GitHub链接

线程的同步与互斥,学习生产者消费者模型及应用场景

线程安全:生产者与消费者模型,读写者模型,同步与互斥的实现,互斥锁,条件变量,posix信号量,读写锁,自旋锁

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归
属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成
线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。
线程共享线程所处的进程的虚拟地址空间,这些公共资源缺乏数据的访问控制容易造成数据混乱,因此为了解决线程安全提出来生产者与消费者模型:

1、生产者与消费者模型

一个场所,二个角色,三个关系:生产者与消费者争抢同一个临界区的临界资源,
生产者与生产者都在抢着访问操作同一个资源,生产者与生产者之间的关系:互斥关系
生产者与消费者:同步关系+互斥关系
消费者与消费者:互斥关系

在这里插入图片描述

为了保证维持生产者与消费者之间的关系来解决数据的安全访问操作,因此提出了同步与互斥,同步就是解决时序访问问题;互斥就是解决同一资源同一时间的唯一访问性问题。
解决线程的安全问题实际就是映射模型中的关系来解决
同步原则保证了不会产生饥饿问题,互斥原则保证了访问原子性。锁变量本身必须是原子操作。

2、读写者模型

适用场景:在编写多线程的时候,有⼀种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有⼀种⽅法,可以专门处理这种多读少写的情况呢? 有,
那就是读写锁。读写锁本质上是⼀种⾃旋锁

在这里插入图片描述

注意:写独占,读共享,写锁优先级⾼
读写锁就是基于自旋锁实现的
自旋锁是一直轮询判断,非常消耗CPU资源,是用于确定等待花费时间比较少,很快就能获取到锁的这种情况。互斥锁是挂起等待。
/*  这是一个验证读写锁的代码
 *      1. 读写锁的初始化
 *      2. 读写锁的操作(加读锁/加写锁/解锁)
 *      3. 读写锁的释放
 *  特性:
 *      写独占,读共享,写优先级高
 *  有多个写线程,多个读线程,验证特性
 */

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

int ticket = 100;
pthread_rwlock_t rwlock;

void *thr_write(void *arg)
{
    while(1) {
        //加写锁
        //int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
        //int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
        //pthread_rwlock_wrlock(&rwlock);//写独占
        //pthread_rwlock_rdlock(&rwlock);//读共享
        pthread_rwlock_wrlock(&rwlock);//写独占

        if (ticket > 0 ) {
            sleep(5);
            ticket--;
            printf("ticket:%d\n", ticket);
        }
        printf("this is write!!\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(5);
    }
    return 0;
}
void *thr_read(void *arg)
{
    while(1) {
        pthread_rwlock_rdlock(&rwlock);

        if (ticket > 0) {
            sleep(5);
            ticket--;
            printf("ticket:%d\n", ticket);
        }
        printf("this is read!!!\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(5);
    }
    return 0;
}
int main()
{
    pthread_t wtid[4], rtid[4];
    int ret, i;

    //1. 读写锁的初始化
    //int    pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
    //          const pthread_rwlockattr_t *restrict attr);

    pthread_rwlock_init(&rwlock, NULL);
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&wtid[i], NULL, thr_write, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&rtid[i], NULL, thr_read, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }

    pthread_join(wtid[0], NULL);
    pthread_join(wtid[1], NULL);
    pthread_join(wtid[2], NULL);
    pthread_join(wtid[3], NULL);
    pthread_join(rtid[0], NULL);
    pthread_join(rtid[1], NULL);
    pthread_join(rtid[2], NULL);
    pthread_join(rtid[3], NULL);
    //3. 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

实际中使用互斥锁(阻塞等待)还是读写锁(自旋锁)取决于正在占用锁的线程占用锁的时间。

实现线程间的互斥:互斥锁(互斥量)mutex

1、 定义一个互斥锁

pthread_mutex_t mutex;

2、初始化互斥锁

互斥锁的初始化:
1、定义时候直接赋值初始化,最后不需要手动释放
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2、函数接口初始化,最后需要手动释放
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
mutex:互斥锁变量
attr:互斥锁属性,可以置空NULL
成功返回:0 错误:errno

3、对临界操作进行加锁/解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

阻塞加锁,如果获取不到锁则阻塞等待锁被解开

int pthread_mutex_trylock(pthread_mutex_t *mutex);
非阻塞加锁,如果获取不到锁则立即报错返回EBUSY
int pthread_mutex_timedlock (pthread_mutex_t *mutex,struct timespec *t);
限时阻塞加锁,如果获取不到锁则等待指定时间,在这段时间内如果一直获取不到,则报错返回,否则加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
解锁

4、释放互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
实现线程间的同步:条件变量、posix信号量
/*这是一个火车站黄牛买票的栗子
 * 每一个黄牛都是一个线程,在这个栗子中有一个总票数ticket
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>

int ticket = 100;

pthread_mutex_t mutex;//定义一个锁变量

void* sale_ticket(void* arg)
{
    int id = (int)arg;
    while(1){
    pthread_mutex_lock(&mutex);//加锁
        if(ticket > 0){
            usleep(100);
            printf("Yellow cow %d get a ticket    ticket:%d\n",id,ticket);
            ticket--;
        }else{
            printf("have no ticket:%d\n",ticket);
            pthread_mutex_unlock(&mutex);//有可能退出,需要解锁,否则会死锁
            pthread_exit(NULL);
        }
    pthread_mutex_unlock(&mutex);//解锁
    }
    return NULL;
}

int main()
{
    pthread_t tid[4];
    int i = 0;

    pthread_mutex_init(&mutex,NULL);//使用函数初始化锁
    for(i=0;i<4;i++)
    {
        int ret=pthread_create(&tid[i],NULL,sale_ticket,(void*)i);
        //不能传i的地址,如果传i的地址,线程函数在调用这个地址时候都是3
        if( ret !=0){
            perror("pthread_create error");
            exit(-1);
        }
    }

    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);
    pthread_join(tid[2],NULL);
    pthread_join(tid[3],NULL);


    pthread_mutex_destroy(&mutex);//销毁锁
    return 0;
}
对互斥锁进行操作的时候,有加锁就一定要有解锁,并且必须在任意一个有可能会退出的地方都要进行解锁操作,否则会造成其它线程的死锁。
死锁情况:因为一直获取不到锁资源而造成的锁死情况

死锁的必要条件:必须具备条件才能满足

1. 互斥条件-----我获取了锁你就不能再获取
2. 不可剥夺条件----我拿到了锁别人不能释放我的锁
3. 请求与保持条件----拿了锁1又去获取锁2,如果没有获取到锁2不释放锁1
4. 环路等待条件----a拿了锁1去请求锁2,b拿了锁2区求锁1
预防产生死锁:破坏请求与保持条件
避免产生死锁的经典实例:银行家算法

实现线程间的同步:条件变量

在这里插入图片描述

需要一个条件:表示临界区有没有资源
为什么条件变量要和互斥锁搭配使用?

>因为等待需要被唤醒,而被唤醒的前提条件就是条件已经满足,并且这个条件本身就是一个临界资源,因此改变条件的操作需要被保护。

条件变量的初始化及销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t * cond,const pthread_condattr_t * attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

没有资源就去阻塞等待:

int pthread_cond_wait(pthread_cond_t * cond,pthread_mutex_t * mutex);
//解锁+等待,当被唤醒时候,它自动获得锁

还有限时等待的函数:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);

pthread_cond_wait函数会对互斥锁做判断,如果调用线程加锁,就解锁,然后陷入等待,整个过程是原子操作,防止消费者先拿到锁,发现条件变量不满足,无法消费,它陷入阻塞等待,这时候生产者得不到锁。

唤醒在条件变量上的线程:

int pthread_cond_broadcast(pthread_cond_t *cond);//广播唤醒
int pthread_cond_signal(pthread_cond_t *cond);//唤醒第一个等待的条件变量的线程
条件变量代码演示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>

pthread_cond_t cond;
pthread_mutex_t mutex;
int basket = 0;//公共条件,需要互斥锁来保证原子性

void* saler(void*arg)
{
    while(1){
        pthread_mutex_lock(mutex);
        if(basket == 0){
            printf("I sell a good\n");
            basket = 1;
            pthread_cond_signal(&cond);
        pthread_mutex_unlock(mutex);
        }
    }
    return NULL;
}

void* customer(void*arg)
{
    while(1){
        pthread_mutex_lock(mutex);
        if(basket == 0){
            //初始状态等待,睡眠
            //pthread_cond_wait函数会对互斥锁做判断,如果调用线程加锁,就解锁,然后陷入等待,整个过程是原子操作
            pthread_cond_wait(&cond,&mutex);
        }
        printf("I bought a gift for my girl friend!!\n");
        basket = 0;
        pthread_mutex_unlock(mutex);
    }
    return NULL;
}

int main()
{
    pthread_t t1,t2;
    int ret;
    pthread_cond_init(&cond,NULL);//条件变量初始化
    pthread_mutex_init(&mutex,NULL);

    ret = pthread_create(&t1,NULL,saler,NULL);
    if(ret != 0){
        perror("pthread_create error");
        exit(-1);
    }

    ret = pthread_create(&t1,NULL,customer,NULL);
    if(ret != 0){
        perror("pthread_create error");
        exit(-1);
    }
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

posix信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。 但POSIX可以用于线程间同步。systemV标准posix实现进程间通信
即可以实现同步也可以实现互斥,既可以用于进程间同步与互斥,也可以用于线程间的同步与互斥。
信号量是什么(具有一个等待队列的计数器)

posix线程同步实现

消费者:没有资源则等待
生产者:生产出来则通知队列中的等待者

1、信号量的初始化

int sem_init(sem_t *sem, int pshared, unsigned int value);
If pshared has the value 0, then the semaphore is shared between the threads of a process
If pshared is nonzero, then the semaphore is shared between processes
sem:信号量变量名
value:信号量初始计数
成功:0 失败:-1

2、信号量的操作(等待/通知)

等待:对于消费者,没有资源则等待。等待信号量,会将信号量的值减1。
int sem_wait(sem_t *sem);//阻塞等待
int sem_trywait(sem_t *sem);//没有资源,报错返回
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//限时等待,超时报错返回
发布信号量:生产者生产出资源,通知消费者。发布信号量,表示资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);

3、信号量的释放

int sem_destroy(sem_t *sem);
Link with -pthread.
/*信号量实现线程同步与互斥
 * 同步:
 *      1、信号量的初始化
 *      2、信号量的操作(等待/通知)
 *      3、信号量的释放
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;//定义信号量

void*thr_producer(void*arg)
{
    while(1){
        sleep(1);
        printf("I make a hot beef noodles!!\n");
        sem_post(&sem);
    }
    return NULL;
}

void*thr_customer(void*arg)
{
    while(1){
        sem_wait(&sem);
        printf("It is declicious!!\n");
    }
    return NULL;

}

int main()
{
    pthread_t t1,t2;
    int ret;

    sem_init(&sem,0,0);//信号量初始化
    ret = pthread_create(&t1,NULL,thr_producer,NULL);
    if(ret != 0 ){
        perror("pthread_create error");
        exit(-1);
    }

    ret = pthread_create(&t2,NULL,thr_customer,NULL);
    if(ret != 0 ){
        perror("pthread_create error");
        exit(-1);
    }

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    sem_destroy(&sem);
    
    return 0;
}

posix线程互斥实现

信号量的操作(等待+通知)
sem_wait()信号量减1
sem_post()发布信号量,表示资源使⽤完毕,可以归还资源了。将信号量值加1。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem;//定义信号量

int ticket = 100;//黄牛抢票,总票数100

void* buy_ticket(void*arg)
{
    int id = (int)arg;
    while(1){
        sem_wait(&sem);
        if(ticket > 0){
            usleep(1000);
            ticket--;
            printf("%d Buy a ticket,the ticket has %d\n",id,ticket);
        }
        sem_post(&sem);
    }
    return NULL;
}
int main()
{
    pthread_t tid[4];
    int ret;
    sem_init(&sem,0,1);//信号量初始化

    int i = 0;
    for(i=0;i<4;i++){
        ret = pthread_create(&tid[i],NULL,buy_ticket,(void*)i);
        if(ret != 0 ){
            perror("pthread_create error");
            exit(-1);
        }
    }
    pthread_join(tid[0],NULL);
    pthread_join(tid[1],NULL);
    pthread_join(tid[2],NULL);
    pthread_join(tid[3],NULL);
    sem_destroy(&sem);

    return 0;
}

信号量与条件变量在保证同步时候的区别:信号量是修改自己内部的资源计数,这个内部的资源计数就是条件,而条件变量修改的是外部的条件,需要我们用户来修改

STL自身不能保证原子性

信号量的操作是一个原子操作。

猜你喜欢

转载自blog.csdn.net/Vickers_xiaowei/article/details/84891592