多线程编程3:C++11 互斥锁和条件变量

1、多线程线程安全问题:

  • 一个全局整型变量自增自减的汇编:
    int count = 100;
    count--;
    //等价于
       mov eax, count
       sub count,1
       mov count,eax
    
    • 在汇编执行的过程中,线程都可能由于时间片用完而让出cpu
    • 假设有两个线程,第一个线程执行到 sub count,1 的时候,就让出cpu,没将值返回内存,导致第二个线程也是从100开始减的,两个线程执行的--操作最后的结果只有99,这种情况就是竞态条件,多线程执行的结果是一致的,不会随着cpu对线程不同的调用顺序,而产生不同的运行结果

2、前置知识:Linux下的互斥锁和条件变量

  • 互斥锁mutex
    • 初始化和销毁

      #include
      // 初始化方法一,直接给互斥体变量赋值
      pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
      // 初始化方法二,通过初始化函数初始化
      int pthread_mutex_init(pthread_mutex_t* restrict mutex, 
      					const pthread_mutexattr_t* restrict attr);
      	- 函数成功返回0,失败返回一个错误码
      	- 第二个参数用来获取互斥体属性,默认设置为NULL
      //销毁通过初始化函数初始化的互斥体对象
      int pthread_mutex_destroy(pthread_mutex_t* mutex);
      	- 函数执行成功返回0,失败返回一个错误码
      
    • 加锁解锁操作

      // 加锁
      int pthread_mutex_lock(pthread_mutex_t* mutex);
       //尝试加锁,没加锁就加锁,被锁住了,就返回错误码EBUSY,不会被阻塞,
      int pthread_mutex_trylock(pthread_mutex_t* mutex);
      // 解锁
      int pthread_mutex_unlock(pthread_mutex_t* mutex);
      // 上面的三个函数,成功返回0,失败返回一个错误码,表示失败原因
      
    • 互斥体属性设置:

      // 初始化属性变量attr
      int pthread_mutexattr_init(pthread_mutexattr_t* attr);
      // 设置 attr 属性类型type
      int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
      // 获取attr属性类型type
      int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict attr, 
      							int* restrict type);
      // 销毁attr属性变量
      int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
      
    • mutex的属性:

      • PTHREAD_MUTEX_NORMAL:普通锁,mutex的默认属性,等价于pthread_mutex_init的第二个参数设置为NULL, 相同线程重复加锁阻塞;
      • PTHREAD_MUTEX_ERRORCHECK:检错锁,相同线程重复加锁,会返回EDEADLK错误码;
      • PTHREAD_MUTEX_ERRORCHECK:嵌套锁,允许同一个线程进行重复加锁,没加锁一次,互斥体对象的引用计数加1,加几次锁,就需要解锁几次,其他线程才能获取找个锁
    • mutex基本使用(普通锁):

      #include 
      #include 
      #include 
      #include 
      
      pthread_mutex_t mtx; //初始化全局互斥体变量
      int             resourceNo = 0; // 全局整型变量
      
      void* worker_thread(void* param)
      {
          pthread_t threadID = pthread_self();// 线程id
      
          printf("thread start, ThreadID: %d\n", threadID);
      
          while (true)
          {
              pthread_mutex_lock(&mtx); //加锁
      
              printf("Mutex lock, resourceNo: %d, ThreadID: %d\n"
              							, resourceNo, threadID);
              resourceNo++;
      
              printf("Mutex unlock, resourceNo: %d, ThreadID: %d\n"
              							, resourceNo, threadID);
      
              pthread_mutex_unlock(&mtx); //解锁
      
              //休眠1秒
              sleep(1);
          }
      
          return NULL;
      }
      
      int main()
      {
          pthread_mutexattr_t mutex_attr; //定义mutex属性变量
          pthread_mutexattr_init(&mutex_attr); //初始化属性变量
          //设置属性
          pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_NORMAL); 
          pthread_mutex_init(&mtx, &mutex_attr); // 初始化mutex,并设置对应属性
      
          //创建5个工作线程
          pthread_t threadID[5];
      
          for (int i = 0; i < 5; ++i)
          {
           //创建线程,等待调度
              pthread_create(&threadID[i], NULL, worker_thread, NULL);
          }
      
          for (int i = 0; i < 5; ++i)
          {
              pthread_join(threadID[i], NULL); //等待回收线程
          }
      
          pthread_mutex_destroy(&mtx); //销毁mutex互斥体
          pthread_mutexattr_destroy(&mutex_attr); //销毁
      
          return 0;
      }
      
    • TIP:

      • 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量无须销毁;
      • 不要去销毁一个已经加锁或正在被条件变量使用的互斥体对象, 不然会返回EBUSY错误码
      • 与只有一个资源数的信号量相比,mutex只能做到同步,不能做到通信
  • 条件变量condition_variable:需要配合mutex使用
    • 初始化和销毁

      #include
      // 定义条件变量对象
      pthread_cond_t cond;
      // 初始化条件变量
      int pthread_cond_init(pthread_cond_t* cond, 
      	const pthread_condattr_t* attr);
      // 销毁条件变量
      int pthread_cond_destroy(pthread_cond_t* cond);
      
    • 等待条件变量

      // 阻塞等待,释放锁,如果条件满足了被唤醒,会直接获得锁
      int pthread_cond_wait(pthread_cond_t* restrict cond, 
      						pthread_mutex_t* restrict mutex);
      // 超时等待
      int pthread_cond_timedwait(pthread_cond_t* restrict cond, 
      						pthread_mutex_t* restrict mutex, 
      						const struct timespec* restrict abstime);
      
    • 条件唤醒

      // 一次唤醒一个,随机唤醒
      int pthread_cond_signal(pthread_cond_t* cond);
      // 唤醒所有线程
      int pthread_cond_broadcast(pthread_cond_t* cond);
      
    • TIP:

      • pthread_cond_wait阻塞的时候,会先释放互斥体,再阻塞,所以需要先加锁再使用条件变量
      • 收到信号,pthread_cond_wait会返回并对绑定的互斥体进行加锁
      • 虚假唤醒:操作系统可能会在一些情况下,唤醒没满足条件,但是还在pthread_cond_wait阻塞的线程,所以一般需要配合whlie循环和pthread_cond_wait;
      • 如果有信号产生的时候,没有及时pthread_cond_wait,则可能导致信号丢失,并永久等待,因此一般需要在pthread_cond_signal或pthread_cond_broadcast之前调用pthread_cond_wait

3、C++11 互斥锁和条件变量

底层是不同的操作系统的具体实现,可用于跨平台

  • 互斥锁mutex

    • 头文件及基本使用:用于保证多线程中临界资源的使用的串行

      #include 
      std::mutex mtx; // 定义的全局的互斥锁
      mtx.lock();// 
      /*
      	临界资源
      */
      mtx.unlock();//解锁
      
    • 裸的mutex对象的使用,存在一个问题:就是代码段中间存在return,会导致不能执行到mtx.unlock()解锁,继而导致死锁问题

    • 相同线程重复lock, linux上会阻塞,windows上程序会崩溃

  • 因此有了lock_gaurdunique_lock两个对象对mutex进行封装,类似于智能指针,构造加锁,出作用域析构自动解锁

    • lock_gaurd:对mutex对象可以进行简单的封装,构造加锁,析构结构

    • unique_lock:不仅对mutex对象 进行简单的封装,还提供了一系列的成员函数,比如加锁解锁等操作

    • 两个对象的差别:

      • lock_gaurd左值引用的赋值和拷贝全部delete,只能用于简单的临界区加锁解锁中,不可能用在函数参数传递,或者返回过程中,函数参数传递和返回过程中,都需要用到拷贝构造或者赋值函数
      • unique_lock左值引用的赋值和拷贝全部delete,但是提供了右值拷贝和右值赋值运算重载,不仅可以使用在简单的临界区互斥的代码上,还可以函数调用过程中使用,此外还这个对象还提供了lock和unlock的成员方法
    • 基本使用:

      int ticketcount = 100; //车站有100张车票,由三个窗口一起卖
      std::mutex mtx; //全局锁
      
      void sellTicket(int index)
      {
          
          while (ticketcount > 0)
          {
              mtx.lock(); 
              /*加锁, 当ticketCount为1的时候,临界区可能有线程在操作,而这里也可能
              有线程在阻塞,最后导致ticketCount为-1, 所以需要在锁里面再进行判断
              ,也就是锁+双重判断*/
              if (ticketcount > 0) //第二重判断
              {
                  std::cout << "窗口:" << index << " 卖出第" 
                  			<< ticketcount << " 张票" << std::endl;
                  ticketcount--;
              }
              mtx.unlock();
               //睡眠100ms
              std::this_thread::sleep_for(std::chrono::milliseconds(100));
          }
          
      }
      void sellTicket2(int index)
      {
      
          while (ticketcount > 0)
          {
              {
                  //std::lock_guard lock(mtx);
                  std::unique_lock<std::mutex> lock(mtx); 
                  if (ticketcount > 0) //第二重判断
                  {
                      std::cout << "窗口:" << index << " 卖出第" 
                      			<< ticketcount << " 张票" << std::endl;
      
                      ticketcount--;
                  }
              }
              std::this_thread::sleep_for(std::chrono::milliseconds(100)); //睡眠100ms
          }
      
      }
      int main()
      {
          std::list<std::thread> t_list;
          for (int i = 0; i < 3; ++i)
          {
              t_list.push_back(std::thread(sellTicket,i));
          }
      
          for (std::thread& t : t_list)
          {
              t.join();
          }
          std::cout << "所有窗口卖票结束" << std::endl;
          return 0;
      }
      
  • 条件变量condition_variable

    • 头文件和基本使用,基本和linux下的一致

      #include 
      #include 
      std::mutex mtx; 
      std::condition_variable cv; 
      class Queue 
      {
      public:
          void put(int val) //生产
          {
              std::lock_guard<std::mutex> guard(mtx);
              std::unique_lock<std::mutex> lck(mtx);
              while (!que.empty())
              { 
                  cv.wait(lck); //进入等待状态就会将这把锁释放掉,必须是unique_lock
              }
              que.push(val);
              /*
                  notify_one:通知一个线程
                  notify_all:通知所有线程
              */
              cv.notify_all();  
              std::cout << "生产者 生产:" << val << "号物品" << std::endl;
          }
          int get()  //消费
          {
              //std::lock_guard guard(mtx);
              std::unique_lock<std::mutex> lck(mtx);
              while (que.empty())
              {
                  //消费者线程发现que是空的,通知生产者线程先生产物品才能消费
                  // 先进入等待状态,再把mutex释放掉
                  cv.wait(lck); //进入等待状态就会将这把锁释放掉
              }
              int val = que.front();
              que.pop();
              cv.notify_all(); //通知其他线程消费完了
              std::cout << "消费者 消费:" << val << "号物品" << std::endl;
              return val;
          }
      private:
          std::queue<int> que;
      };
      void producer(Queue* que) //生产者线程
      {
          for (int i = 1; i <= 10; ++i)
          {
              que->put(i);
              std::this_thread::sleep_for(std::chrono::microseconds(100));
          }
      }
      void comsumer(Queue* que) // 消费者线程
      {
          for (int i = 1; i <= 10; ++i)
          {
              que->get();
              std::this_thread::sleep_for(std::chrono::microseconds(100));
          }
      }
      int main()
      {
          Queue que; //两个线程共享的队列
          std::thread t1(producer,&que);
          std::thread t2(comsumer,&que);
          t1.join();
          t2.join();
          return 0;
      }
      

你可能感兴趣的:(多线程编程,c++)