• C++(Qt)线程与锁


    简单线程了解

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>         //创建两个线程,分别对两个全变量进行++操作,判断两个变量是否相等,不相等打印
     
    int a = 0;
    int b = 0;
    // 未初始化 和0初始化的成员放在bbs
    pthread_mutex_t mutex;
     
    void* route()
    {
        while(1)            //初衷不会打印
        {
            a++;
            b++;
            if(a != b)
            {
                printf("a =%d, b = %d
    ", a, b);
                a = 0;
                b = 0;
            }
        }
    }
     
    int main()
    {
        pthread_t tid1, tid2;
        pthread_create(&tid1, NULL, route, NULL);
        pthread_create(&tid2, NULL, route, NULL);
        
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
        return 0;
    }
    

      段代码的运行结果优点出乎我们的预料:

    我们预计的结构应该是不会打印的,而这里去打印出了我们意想不到的结果。连相等的数据都打印了出来,为什么会出现这样的情况呢?

    解释:两个线程互相抢占CPU资源,一个线程对全局变量做了++操作之后,还没来得及比较输出操作,另一个线程抢占CPU,进行比较打印输出。为了避免这样的情况,就需要用到下面介绍的互斥锁。

    互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。

    1.定义互斥量: pthread_mutex_t mutex;
    2.初始化互斥量: pthread_mutex_init(&mutex, NULL); //第二个参数不研究置NULL;          //初始化为 1 (仅做记忆)
    3.上锁      pthread_mutex_lock(&mutex);   1->0;    0   等待
    4.解锁           pthread_mutex_unlock(&mutex);   置1 返回

    5.销毁           pthread_mutex_destroy(&mutex);  

    返回值:若成功返回0,若出错返回错误编号。

    说明: 互斥锁,在多个线程对共享资源进行访问时,在访问共享资源前对互斥量进行加锁,在访问完再进行解锁,在互斥量加锁后其他的线程将阻塞,直到当前的线程访问完毕并释放锁。如果释放互斥锁时有多个线程阻塞,所有阻塞线程都会变成可运行状态,第一个变成可运行状态的线程可以对互斥量加锁。这样就保证了每次只有一个线程访问共享资源。

    至此,我们好像能通过互斥锁解决上面的问题:

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
     
    int a = 0;
    int b = 0;
    // 未初始化 和0初始化的成员放在bbs
    pthread_mutex_t mutex;
     
    void* route()
    {
        while(1)            //初衷不会打印
        {
            pthread_mutex_lock(&mutex);   
            a++;
            b++;
            if(a != b)
            {
                printf("a =%d, b = %d
    ", a, b);
                a = 0;
                b = 0;
            }
            pthread_mutex_unlock(&mutex);
        }
    }
     
    int main()
    {
        pthread_t tid1, tid2;
        pthread_mutex_init(&mutex, NULL);
        pthread_create(&tid1, NULL, route, NULL);
        pthread_create(&tid2, NULL, route, NULL);
        
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
        pthread_mutex_destroy(&mutex);
     
        return 0;
    }
    

      现有如下场景:线程1和线程2,线程1执行函数A,线程2执行函数B,现只使用一把锁,分别对A,B函数的执行过程加锁和解锁。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>           //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞
     
    pthread_mutex_t mutex;
     
    void* odd(void* arg)
    {
      int i = 1;
      for(; ; i+=2)
      {
        pthread_mutex_lock(&mutex);
        printf("%d
    ", i);
        pthread_mutex_unlock(&mutex);
      }
    }
     
    void* even(void* arg)
    {
      int i = 0;
      for(; ; i+=2)
      {
        pthread_mutex_lock(&mutex);
        printf("%d
    ", i);
        pthread_mutex_unlock(&mutex);
      }
    }
     
     
    int main()
    {
        pthread_t t1, t2;
        pthread_mutex_init(&mutex, NULL);
        pthread_create(&t1, NULL, even, NULL);
        pthread_create(&t2, NULL, odd, NULL);
        //pthread_create(&t3, NULL, even, NULL);
        
        sleep(3);
        pthread_cancel(t2);             //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前
     
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
        pthread_mutex_destroy(&mutex);
     
        return 0;
    }
    

      

    一种极限情况是:线程2的取消发生在线程2的解锁之前,那么就会导致因为锁没解开,而线程1无法继续运行。

    解决这样的问题我们可以用到下面的宏函数:

    宏:              //注册线程回调函数,可用来防止线程取消后没有解锁的问题

    void pthread_cleanup_push(void (*routine)(void *),  //回调函数
                                                  void *arg); //回调函数的参数
    //回调函数执行时机
               1.pthread_exit
               2.pthread_cancel
               3.cleanaup_pop参数不为0,当执行到cleaup_pop时,调用回调函数

    void pthread_cleanup_pop(int execute);

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>           //线程的取消动作发生在加锁和解锁过程中时,当发生线程2取消后而没有进行解锁时,就会出现线程1将一直阻塞
     
    pthread_mutex_t mutex;
     
    void callback(void* arg)      //在cancel中进行解锁
    {
      printf("callback
    ");
      sleep(1);
      pthread_mutex_unlock(&mutex); 
    }
     
    void* odd(void* arg)
    {
      int i = 1;
      for(; ; i+=2)
      {
        pthread_cleanup_push(callback, NULL);//因为调用了cancel函数,从而触发了回调函数。
        pthread_mutex_lock(&mutex);
        printf("%d
    ", i);
        pthread_mutex_unlock(&mutex);
        pthread_cleanup_pop(0);
      }
    }
     
    void* even(void* arg)
    {
      int i = 0;
      for(; ; i+=2)
      {
        pthread_mutex_lock(&mutex);
        printf("%d
    ", i);
        pthread_mutex_unlock(&mutex);
      }
    }
     
     
    int main()
    {
        pthread_t t1, t2;
        pthread_mutex_init(&mutex, NULL);
        pthread_create(&t1, NULL, even, NULL);
        pthread_create(&t2, NULL, odd, NULL);
        //pthread_create(&t3, NULL, even, NULL);
        
        sleep(3);
        pthread_cancel(t2);             //取消线程2,这个动作可能发生在线程2加锁之后和解锁之前
        //pthread_mutex_unlock(&mutex);   有问题,如果执行even的程序有两个,而一个取消线程的函数执行时正好t3函数阻塞,就会导致t3和t1同时在执行even
     
        pthread_join(t1, NULL);
        pthread_join(t2, NULL);
        pthread_mutex_destroy(&mutex);
     
        return 0;
    }
    

      

    注意:

    1.不要销毁一个已经加锁的互斥量,销毁的互斥量确保后面不会再有线程使用。

    2.上锁和解锁函数要成对的使用

    3.选择合适的锁的粒度(数量)。如果粒度太粗,就会出现很多线程阻塞等待相同锁,源自并发性的改善微乎其微。如果锁的粒度太细,那么太多的锁的开销会使系统的性能受到影响,而且代码会变得相当复杂。

    4.加锁要加最小(范围)锁,减少系统负担

    使用互斥锁一定要注意避免死锁:《Linux高性能服务器编程》  14.5.3 介绍了两个互斥量因请求顺序产生死锁问题

              如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁。例如,程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。

              可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。例如,假设需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量不会产生死锁(当然在其他资源上仍可能出现死锁);类似地,如果所有的线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。

             为了应对死锁,在实际的编程中除除了加上同步互斥量之外,还可以通过以下三原则来避免写出死锁的代码:

    1>短:写的代码尽量简洁

    2>平:代码中没有复杂的函数调用

    3>快:代码的执行速度尽可能快

    自旋锁:  应用在实时性要求较高的场合(缺点:CPU浪费较大)

    pthread_mutex_spin;

    pthread_spin_lock() ; //得不到时,进入忙等待,不断向CPU进行询问请求

    pthread_spin_unlock(); 

     pthread_spin_destroy(pthread_spinlock_t *lock);

    pthread_spin_init(pthread_spinlock_t *lock, int pshared);

    读写锁(共享-独占锁):应用场景---大量的读操作  较少的写操作

    注意:读读共享, 读写互斥,写优先级高(同时到达)

    1. pthread_rwlock_t rwlock;//定义

    2.int pthread_rwlock_init()//初始化

    3.pthread_rwlock_rdlock()//pthread_rwlock_wrlock//读锁/写锁

    4.pthread_rwlock_unlock() // 解锁

    5.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁锁

    返回值:成功返回0,出错返回错误编号

    说明:不管什么时候要增加一个作业到队列中或者从队列中删除作业,都用写锁,

    不管何时搜索队列,首先获取读模式下的锁,允许所有的工作线程并发的搜索队列。在这样的情况下只有线程

    搜索队列的频率远远高于增加或删除作业时,使用读写锁才可能改善性能。

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>                //创建8个线程,3个写线程,5个读线程
     
    pthread_rwlock_t rwlock;
    int counter = 0;
     
    void* readfunc(void* arg)
    {
      int id = *(int*)arg;
      free(arg);
      while(1)
      {
        pthread_rwlock_rdlock(&rwlock);
        printf("read thread %d : %d
    ", id, counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(100000);
      }
    }
     
    void* writefunc(void* arg)
    {
      int id = *(int*)arg;
      free(arg);
      while(1)
      {
        int t = counter;
        pthread_rwlock_wrlock(&rwlock);
        printf("write thread %d : t= %d,  %d
    ", id, t, ++counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(100000);
      }
    }
    int main()
    {
        pthread_t tid[8];
        pthread_rwlock_init(&rwlock, NULL);
        int i = 0;
        for(i = 0; i < 3; i++)
        {
          int* p =(int*) malloc(sizeof(int));
          *p = i;
          pthread_create(&tid[i], NULL, writefunc, (void*)p);
        }
        for(i = 0; i < 5; i++)
        {
          int* p = (int*)malloc(sizeof(int));
          *p = i;
          pthread_create(&tid[3+i], NULL, readfunc, (void*)p);
        }
     
        for(i = 0; i < 8; i++)
        {
          pthread_join(tid[i], NULL);
        }
     
        pthread_rwlock_destroy(&rwlock);
     
        return 0;
    }
    

      

    条件变量:  如果说互斥锁是用于同步线程对共享数据的访问的化,那么条件变量这是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程       

    1.定义条件变量  pthread_cond_t cond;
    2.初始化        pthread_cond_init(&cond, NULL);
    3.等待条件      pthread_cond_wait(&cond, &mutex);
                                     mutex :如果没有在互斥环境,形同虚设
                                     在互斥环境下:wait函数将mutex置1,wait返回,mutex恢复成原来的值
    4.修改条件      pthread_cond_signal(&cond);
    5.销毁条件      pthread_cond_destroy(&cond);

    //规范写法:
    pthread_mutex_lock();
        while(条件不满足)
        pthread_cond_wait();
    //为什么会使用while?
    //因为pthread_cond_wait是阻塞函数,可能被信号打断而返回(唤醒),返回后从当前位置
    //向下执行, 被信号打断而返回(唤醒),即为假唤醒,继续阻塞
    pthread_mutex_unlock();
     
    pthread_mutex_lock();
    pthread_cond_signal(); //信号通知   ----   如果没有线程在等待,信号会被丢弃(不会保存起来)。
    pthread_mutex_unlock();
    

      

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <stdlib.h>             //创建两个线程一个wait print,一个signal sleep()
     
    pthread_cond_t cond;
    pthread_mutex_t mutex;
     
    void* f1(void* arg)
    {
      while(1)
      {
        pthread_cond_wait(&cond, &mutex);
        printf("running!
    ");
      }
    }
    void* f2(void* arg)
    {
      while(1)
      {
        sleep(1);
        pthread_cond_signal(&cond);
      }
    }
     
    int main()
    {
      pthread_t tid1, tid2;
      pthread_cond_init(&cond, NULL);
      pthread_mutex_init(&mutex, NULL);
     
      pthread_create(&tid1, NULL, f1, NULL);
      pthread_create(&tid2, NULL, f2, NULL);
     
      pthread_join(tid1, NULL);
      pthread_join(tid2, NULL);
     
      pthread_cond_destroy(&cond);
      pthread_mutex_destroy(&mutex);
      return 0;
    }
    

      

                 System V //基于内核持续性

    信号量:      POSIX    //基于文件持续性的信号量
    1.定义信号量: sem_t sem;
    2,初始化信号量:    sem_init(sem_t* sem,
                                                    int shared,   //0表示进程内有多少个线程使用
                                                    int val);     //信号量初值
    3.PV操作        int sem_wait(sem_t* sem);   //sem--;如果小于0,阻塞    P操作
                          int sem_post(sem_t* sem);   //sem++;                 V操作
    4.销毁            sem_destroy(sem_t* sem);
    信号量实现生产者消费者模型:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <semaphore.h>
    //仓库中装产品编号,没装产品的位置,置为-1,装了的地方置为产品的编号
     
     
    #define PRO_COUNT 3
    #define CON_COUNT 2
    #define BUFSIZE 5
     
     
    sem_t sem_full;     //标识可生产的产品个数
    sem_t sem_empty;    //表示可消费的产品个数
    pthread_mutex_t mutex;  //互斥量
    int num = 0;        //产品编号
    int buf[BUFSIZE];   //仓库
    int wr_idx;     //写索引
    int rd_idx;     //读索引
     
    void* pro(void* arg)
    {
      int i = 0;
      int id = *(int*)arg;
      free(arg);
      while(1)
      {
          sem_wait(&sem_full);    //先判断仓库是否满
          pthread_mutex_lock(&mutex); //互斥的访问具体的仓库的空闲位置
          printf("%d生产者开始生产%d
    ", id, num);
          for(i = 0; i < BUFSIZE; i++)
          {
            printf("	buf[%d]=%d", i, buf[i]);
            if(i == wr_idx)
            {
              printf("<=====");
            }
            printf("
    ");
          }
          buf[wr_idx] = num++;      //存放产品
          wr_idx = (wr_idx + 1) % BUFSIZE;
          printf("%d生产者结束生产
    ", id);
          pthread_mutex_unlock(&mutex);
          sem_post(&sem_empty);
          sleep(rand()%3);
        }
    }
     
    void* con(void* arg)
    {
      int i = 0;
        int id = *(int*)arg;
        free(arg);
        while(1)
        {
            sem_wait(&sem_empty);
            pthread_mutex_lock(&mutex);
            
            printf("%d消费者开始消费%d
    ", id, num);
            for(i = 0; i < BUFSIZE; i++)
            {
              printf("buf[%d]=%d", i, buf[i]);
              if(i == rd_idx)
              {
                printf("=====>");
              }
              printf("
    ");
            }
            int r = buf[rd_idx];
            buf[rd_idx] = -1;
            rd_idx = (rd_idx+1)%BUFSIZE;
            sleep(rand()%4);
            printf("%d
    消费者消费完%d
    ", id, r);
            pthread_mutex_unlock(&mutex);
            sem_post(&sem_full);
            sleep(rand()%2);
        }
    }
     
    int main()
    {
        pthread_t tid[PRO_COUNT+CON_COUNT];
        pthread_mutex_init(&mutex, NULL); //初始化
        sem_init(&sem_empty, 0, 0);
        sem_init(&sem_full, 0, BUFSIZE);
        srand(getpid());
     
        int i = 0;
        for(i = 0; i < BUFSIZE; i++)      //初始化仓库  -1表示没有品
            buf[i] = -1;
     
        for(i = 0; i < PRO_COUNT; i++)    //产生生产者
        {
            int *p = (int*)malloc(sizeof(int));
            *p = i;
            pthread_create(&tid[i], NULL, pro, p);
        }
     
        for(i = 0; i < CON_COUNT; i++)
        {
            int *p = (int*)malloc(sizeof(int));
            *p = i;
            pthread_create(&tid[i+CON_COUNT], NULL, con, p);
        }
        
        for(i = 0; i < PRO_COUNT + CON_COUNT; i++)
        {
            pthread_join(tid[i], NULL);
        }
     
        pthread_mutex_destroy(&mutex);  //销毁
        sem_destroy(&sem_empty);
        sem_destroy(&sem_full);
     
        return 0;
    }
    

      

    拓展学习:

    乐观锁和悲观锁?

    乐观锁:

         在关系数据库管理系统里,乐观并发控制(又名”乐观锁”,Optimistic Concurrency Control,缩写”OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

    乐观并发控制的事务包括以下阶段: 
    1. 读取:事务将数据读入缓存,这时系统会给事务分派一个时间戳。 
    2. 校验:事务执行完毕后,进行提交。这时同步校验所有事务,如果事务所读取的数据在读取之后又被其他事务修改,则产生冲突,事务被中断回滚。 

    3. 写入:通过校验阶段后,将更新的数据写入数据库。

    优点和不足:

           乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

    悲观锁:

        在关系数据库管理系统里,悲观并发控制(又名”悲观锁”,Pessimistic Concurrency Control,缩写”PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

    优点和不足:悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

    系统最多能够创建多少个线程? (一般以实测为准,但根据每次开辟的栈的大小不同,测试结果也会不同)。

    一个是直接在命令行查看    cat /proc/sys/kernel/threads-max  我的电脑显示是 7572

    另一个是自己计算 用户空间大小3G 即是3072M/8M栈空间  = 380     

    第三个写程序:   跑到32754(理论值 32768)

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>               //创建线程
     
    void* foo(void* arg)
    {
    }
     
    int main()
    {
        int count = 0;
        pthread_t thread;
     
        while(1)
        {
            if(pthread_create(&thread, NULL, foo, NULL) != 0)
            return 1;
            count++;
        printf("MAX = %d
    ", count);
        }
     
        return 0;
    }
    

      

    Copyright @WinkJie
  • 相关阅读:
    Git 简介
    Web开发——jQuery基础
    VueJS教程4
    VueJS教程3
    VueJS教程2
    linux命令,系统安全相关命令--改变文件属性与权限(chgrp,chwon,chmod)
    linux命令,系统安全相关命令--su
    linux命令,系统安全相关命令--passwd
    git常用命令整理
    vi常用按键
  • 原文地址:https://www.cnblogs.com/WinkJie/p/14584487.html
Copyright © 2020-2023  润新知