zoukankan      html  css  js  c++  java
  • 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 ", 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 ", 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 ", 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 ", 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

  • 相关阅读:
    用 tableExcel导出EXCEL数据
    个人作业——软件评测
    结对第二次作业——某次疫情统计可视化的实现
    软工实践寒假作业(1/2)
    java注解和反射
    共享密钥
    鲁棒性验证-第五小组
    维数约减报告--第五小组
    网络1911、1912 D&S第1次作业--线性表批改总结
    网络1911、1912 C语言第4次作业--函数批改总结
  • 原文地址:https://www.cnblogs.com/wanghuaijun/p/12332042.html
Copyright © 2011-2022 走看看