c/c++:线程同步(互斥锁、死锁、读写锁、条件变量、生产者和消费者模型、信号量)

目录

1. 概念

2. 互斥锁

3. 死锁

4. 读写锁

5. 条件变量

5.1 生产者和消费者模型

6. 信号量

 
1. 概念

    线程同步:

 > 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作。
  > - 在多个线程操作一块共享数据的时候
  >   - 按照先后顺序依次访问
  >   - 有原来的 并行 -> 串行

    临界资源:一次只允许一个线程使用的资源。
    原子操作:

  > 原子操作,就是说像原子一样不可再细分不可被中途打断。

  > 一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。

 
2. 互斥锁

    互斥锁类型:

    // pthread_mutex_t 互斥锁的类型
    pthread_mutex_t mutex;

    互斥锁特点:让多个线程, 串行的处理临界区资源(一个代码块)
    互斥锁相关函数:

      #include <pthread.h>
     
      // 初始化互斥锁
      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);
     
      // 将参数指定的互斥锁上锁
      // 比如: 3个线程, 第一个线程抢到了锁, 对互斥锁加锁 -> 加锁成功, 进入了临界区
      //  第二,三个个线程也对这把锁加锁, 因为已经被线程1锁定了, 线程2,3阻塞在了这把锁上 -> 不能进入临界区,
      //     当这把锁被打开, 线程2,3解除阻塞, 线程2,3开始抢锁, 谁抢到谁加锁进入临界区, 另一个继续阻塞在锁上
      int pthread_mutex_lock(pthread_mutex_t *mutex);
     
      // 尝试加锁, 如果这把锁已经被锁定了, 加锁失败, 函数直接返回, 不会阻塞在锁上
      int pthread_mutex_trylock(pthread_mutex_t *mutex);
     
      // 解锁函数
      int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中:

      restrict: 修饰符, 被修饰过的变量特点: 不能被其他指针引用
          - mutex变量对应一块内存
          - 举例: pthread_mutex_t* ptr; ptr = &mutex; // error
          -  即便做了赋值, 使用ptr指针操作mutex对应的内存也是不允许的

 
3. 死锁

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁 。

死锁几种场景:

    忘记释放锁,自己将自己锁住
    单线程重复申请锁
    多线程多锁申请, 抢占锁资源(线程A有一个锁1,线程B有一个锁2。线程A试图调用lock来获取锁2就得挂起等待线程B释放,线程B也调用lock试图获得锁1。都在等对方释放,然后获得对方的锁。)

 
4. 读写锁

    读写锁类型? 是几把锁?

          1. 读写锁是一把锁
          2. 锁定读操作, 锁定写操作
          3. 类型: pthread_rwlock_t

    读写锁的特点

    /*
          1. 读操作可以并进行, 多个线程
          2. 写的时候独占资源的
          3. 写的优先级高于读的优先级
    */
    场景:

              // 1. 线程A加读锁成功, 又来了三个线程, 做读操作, 可以加锁成功----读操作是共享的, 三个新来的线程可以加读锁成功
              // 2. 线程A加写锁成功, 又来了三个线程, 做读操作, 三个线程阻塞-------加读锁失败, 会阻塞在读锁上, 写完了
              // 3. 线程A加读锁成功, 又来了B线程加写锁阻塞, 又来了C线程加读锁阻塞------写的独占的, 写的优先级高

    什么时候使用读写锁?

    互斥锁: 数据所有的读写都是串行的
    读写锁:
           - 读: 并行
           - 写: 串行
      读的频率 > 写的频率

    操作函数:

      #include <pthread.h>
      // 初始化读写锁
      int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                 const pthread_rwlockattr_t *restrict attr);
          参数:
              - rwlock: 读写锁地址
              - attr: 读写锁属性, 使用默认属性, 设置为: NULL
     
      // 释放读写锁资源
      int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
     
      // 加读锁
      // rwlock被加了写锁, 这时候阻塞
      // rwlock被加了读锁, 不阻塞, 可以加锁成功 -> 读共享
      int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
     
      // 尝试加读锁
      int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
     
      // 加写锁
      // rwlock -> 加了读锁, 加了写锁 多会阻塞 -> 写独占
      int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
     
      // 尝试加写锁
      int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
     
      // 读写锁解锁
      int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

 

    练习例子:  8个线程操作同一个全局变量,其中3个线程不定时写同一全局资源,其中5个线程不定时读同一全局资源

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <pthread.h>
     
    int number = 1;
    pthread_rwlock_t rwlock;
     
    void* writeNum(void* arg)
    {
        while(1)
        {
            pthread_rwlock_wrlock(&rwlock);
            number++;
            usleep(100);
            printf("+++ write, tid: %ld, number: %d\n", pthread_self(), number);
            pthread_rwlock_unlock(&rwlock);
            usleep(100);
        }
        return NULL;
    }
     
    void* readNum(void* arg)
    {
        while(1)
        {
            pthread_rwlock_rdlock(&rwlock);
            printf("=== read, tid: %ld, number: %d\n", pthread_self(), number);
            pthread_rwlock_unlock(&rwlock);
            usleep(100);
        }
        return NULL;
    }
     
    int main(int argc, char *argv[])
    {
        pthread_t wtid[3], rtid[5];
        //初始化锁
        pthread_rwlock_init(&rwlock, NULL);
        //创建写进程
        for (int i=0; i<3; ++i)
        {
            pthread_create(&wtid[i],NULL, writeNum, NULL);
        }
        //创建读进程
        for (int i=0; i<5; ++i)
        {
            pthread_create(&rtid[i], NULL, readNum, NULL);
        }
     
        //回收进程
        for (int i=0; i<3; ++i)
        {
            pthread_join(wtid[i], NULL);
        }
        for (int i=0; i<5; ++i)
        {
            pthread_join(rtid[i], NULL);
        }
        //销毁锁
        pthread_rwlock_destroy(&rwlock);
        return 0;
    }

 
5. 条件变量

    条件变量不是锁
    条件变量两个动作:

    条件变量能引起某个线程的阻塞具体来说就是:

           - 某个条件满足之后, 阻塞线程
           - 某个条件满足, 线程解除阻塞

    如果使用了条件变量进行线程同步, 多个线程操作了共享数据, 不能解决数据混乱问题,解决该问题, 需要配合使用互斥锁

    条件变量类型

pthread_cond_t

    条件变量操作函数

    #include <pthread.h>
      // 初始化条件变量
      int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
          参数:
              - cond: 条件变量的地址
              - attr: 使用默认属性, 这个值设置为NULL
     
      // 释放资源
      int pthread_cond_destroy(pthread_cond_t *cond);
     
      // 线程调用该函数之后, 阻塞
      int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
          参数:
              - cond: 条件变量
              - mutex: 互斥锁
              
      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);
          参数:
              - cond: 条件变量
              - mutex: 互斥锁
              - abstime: 阻塞的时间
                  - 当前时间 + 要阻塞的时长
                      struct timeval val;
                  可以使用函数:gettimeofday(&val, NULL);
     
      // 唤醒一个或多个阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程
      int pthread_cond_signal(pthread_cond_t *cond);
     
      // 唤醒所有的阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程
      int pthread_cond_broadcast(pthread_cond_t *cond);

 
5.1 生产者和消费者模型

角色分析:
      - 生产者
      - 消费者
      - 容器

    栗子:使用条件量实现 生产线和消费者模型: 生产者往链表中添加节点, 消费者删除链表节点

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <pthread.h>
     
    pthread_cond_t cond;         //条件变量
    pthread_mutex_t mutex;       //互斥锁
     
    //连表节点
    struct Node
    {
        int number;
        struct Node* next;
    };
     
    //指向链表第一个节点的指针
    struct Node* head = NULL;
     
    // 生产者函数、
    void* producer(void* arg)
    {
        while(1)
        {
            //创建新的链表节点
            pthread_mutex_lock(&mutex);
            struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
            pnew->next = head;
            head = pnew;
            pnew->number = rand() % 1000;
            printf("add+++ node, number: %d, tid = %ld\n", pnew->number, pthread_self());
            pthread_mutex_unlock(&mutex);
     
            //生产者生产了东西,通知消费者消费
            pthread_cond_signal(&cond);
        }
        return NULL;
    }
     
    //消费者函数
    void* customer(void* arg)
    {
        while(1)
        {
            pthread_mutex_lock(&mutex);
            while (head == NULL)
            {
                //链表为空,阻塞
                pthread_cond_wait(&cond, &mutex);
            }
     
            struct Node* pnode = head;
            head = head->next;
            printf("del--- node, number: %d, tid = %ld\n", pnode->number, pthread_self());
            free(pnode);
            pthread_mutex_unlock(&mutex);
        }
     
        return NULL;
    }
     
    int main(int argc, char *argv[])
    {
        pthread_t ptid[5], ctid[5];
        pthread_cond_init(&cond,NULL);
        pthread_mutex_init(&mutex,NULL);
     
        for (int i=0; i<5; ++i)
        {
            pthread_create(&ptid[i], NULL, producer, NULL);
            pthread_create(&ctid[i], NULL, customer, NULL);
        }
     
        for (int i=0; i<5; ++i)
        {
            pthread_join(ptid[i], NULL);
            pthread_join(ctid[i], NULL);
        }
        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mutex);
        return 0;
    }

 
6. 信号量

    信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
    信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
    信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用
    信号量主要阻塞线程, 不能完全保证线程安全.
     如果要保证线程安全, 需要信号量和互斥锁一起使用.

 

- 信号量类型:

    sem_t
      在这个变量中记录了一个整形数, 如果这个数据 是5, 允许有五个线程访问数据
              o o o o o
              如果有一线程访问了共享资源, 这个整形数 -1, 后边又有4个线程访问了共享数据 0,
              这时候, 再有线程访问共享数据, 这些线程阻塞

- 信号量操作函数:

    #include <semaphore.h>
      // 初始化信号量
      int sem_init(sem_t *sem, int pshared, unsigned int value);
          参数:
              - sem: 信号量的地址
              - pshared: 0-> 处理线程, 1-> 处理进程
              - value: sem_t中整形数初始化
     
      // 释放资源
      int sem_destroy(sem_t *sem);
     
      // 有可能引起阻塞
      // 调用一次这个函数 sem 中整形数 --
      // 当 sem_wait 并且 sem中的整形数为0 , 阻塞了
      int sem_wait(sem_t *sem);
     
      // 当 sem_trywait 并且 sem中的整形数为0 , 返回, 不阻塞
      int sem_trywait(sem_t *sem);
     
      // 当 sem_timedwait 并且 sem中的整形数为0 , 阻塞一定的时长, 时间到达, 返回
      int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
     
      // 当 sem_post sem 中的整形数 ++
      int sem_post(sem_t *sem);
     
      // 查看 sem中的整形数的值, 通过第二个参数返回
      int sem_getvalue(sem_t *sem, int *sval);
————————————————
版权声明:本文为CSDN博主「陈宸-研究僧」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35883464/article/details/103547949

posted @ 2020-02-19 17:09  专注it  阅读(2150)  评论(0编辑  收藏  举报