C++ 多线程std::thread以及条件变量和互斥量的使用

前言

  • 本文章主要介绍C++11语法中std::thread的使用,以及条件变量和互斥量的使用。

std::thread介绍

构造函数

  • std::thread 有4个构造函数
c 复制代码
// 默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作
thread() noexcept;

// 移动构造函数。将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。
// 线程对象只可移动,不可复制
thread( thread&& other ) noexcept;

// 创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数
template< class F, class... Args > 
explicit thread( F&& f, Args&&... args );

// 使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝
thread( const thread& ) = delete;
  • 通过以下代码演示下如何构造函数的使用
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      
      void threadFunc2() {
      	std::cout << "enter threadFunc2" << std::endl;
      }
      
      void threadFunc3(int data) {
      	std::cout << "enter threadFunc3, data: " << data << std::endl;
      }
      
      class CThread4 {
      public:
      	void threadFunc4(const char * data) {
      		std::cout << "enter threadFunc4, data: " << data << std::endl;
      	}
      };
      
      void threadFunc5() {
      	for (int i = 0; i < 5; i++) {
      		std::cout << "enter threadFunc5" << std::endl;
      		std::this_thread::sleep_for(std::chrono::seconds(1));
      	}
      }
      
      
      int main() {
      
      	// 默认构造
      	std::thread th1;
      
      	// 线程中执行函数
      	std::thread th2(threadFunc2);
      	th2.join();
      
      	// 线程中执行带参函数
      	std::thread th3(threadFunc3, 10010);
      
      	th3.join();
      
      	CThread4 ct4;
      	// 线程中执行类成员函数
      	std::thread th4(&CThread4::threadFunc4, &ct4, "hello world");
      	th4.join();
      
      
      	std::thread th5_1(threadFunc5);
      	// 使用移动构造
      	std::thread th5_2(std::move(th5_1));
      	th5_2.join();
      
      	// 执行lambda表达式
      	std::thread th6([] {
      		std::cout << "enter threadFunc6" << std::endl;
      	});
      	th6.join();
      
      	system("pause");
      	return 0;
      }
  • 执行结果
    *

        enter threadFunc2
        enter threadFunc3, data: 10010
        enter threadFunc4, data: hello world
        enter threadFunc5
        enter threadFunc5
        enter threadFunc5
        enter threadFunc5
        enter threadFunc5
        enter threadFunc6
        请按任意键继续. . .
    

成员函数

c 复制代码
// 获取线程ID
std::thread::id get_id() const noexcept;
// 阻塞当前线程,直至调用join的子线程运行结束
void join();
// 将执行线程从线程对象中分离,允许独立执行。
void detach();
// 判断主线程和子线程的关联状态
bool joinable() const noexcept;
// 如果 *this 仍然有一个关联的运行中的线程,则调用 std::terminate()。
// 否则,将 other 的状态赋给 *this 并将 other 设置为默认构造的状态。
thread& operator=( thread&& other ) noexcept;
  • 通过代码看下如何使用成员函数
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      
      void threadFunc3(int data) {
      	std::cout << "enter threadFunc3, data: " << data << std::endl;
      }
      
      void threadFunc4(int data) {
      	std::cout << "start threadFunc4, data: " << data << std::endl;
      	std::this_thread::sleep_for(std::chrono::seconds(2));
      	std::cout << "end threadFunc4, data: " << data << std::endl;
      }
      
      void threadFunc5(int data) {
      	std::cout << "start threadFunc5, data: " << data << std::endl;
      	std::this_thread::sleep_for(std::chrono::seconds(2));
      	std::cout << "end threadFunc5, data: " << data << std::endl;
      }
      
      int main() {
      	{
      		// 线程中执行带参函数
      		std::thread th3(threadFunc3, 10010);
      		std::cout << "th3 id: " << th3.get_id() << std::endl;
      		// 此刻th3线程与主线程有关联
      		std::cout << "th3 joinable: " << th3.joinable() << std::endl;
      		th3.join();
      		std::cout << "th3 id: " << th3.get_id() << std::endl;
      		// 线程执行结束,此刻th3线程与主线程无关联
      		std::cout << "th3 joinable: " << th3.joinable() << std::endl;
      	}
      
      	{
      		std::thread th5(threadFunc5, 10050);
      		// 如果不想在主线程中等待子线程,可以使用detach。
      		// 这样即便主线程运行结束,子线程依旧会执行
      		// 实际使用时不建议这样做
      		th5.detach();
      	}
      
      	system("pause");
      	return 0;
      }
  • 执行结果
    *

        enter threadFunc3, data: 10010th3 id: 12820
        th3 joinable: 1
        th3 id: 0
        th3 joinable: 0
        start threadFunc5, data: 10050
        请按任意键继续. . . end threadFunc5, data: 10050
    

条件变量

  • 条件变量是C++11提供的一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。

  • C++11中的条件变量叫 condition_variable,需要配合std::unique_lock<std::mutex>使用。

  • 先看以下一段代码
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      #include <mutex>
      #include <condition_variable>
      
      int g_cnt = 0;
      
      // 定义互斥量
      std::mutex g_mutex;
      
      // 定义条件变量
      std::condition_variable g_cond;
      
      void threadFunc1() {
      
      	while (g_cnt != 50) {
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      
      	std::cout << "threadFunc1 g_cnt: " << g_cnt << std::endl;
      }
      
      void threadFunc2() {
      
      	while (g_cnt < 100) {
      		g_cnt++;
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }
      
      int main() {
      	{
      		std::thread th1(threadFunc1);
      		std::thread th2(threadFunc2);
      		th1.join();
      		th2.join();
      
      		std::cout << "g_cnt: " << g_cnt << std::endl;
      	}
      
      	system("pause");
      	return 0;
      }
  • 线程2对g_cnt进行递增操作,线程1在g_cnt等于50时退出循环并打印结果。但这个流程有一个问题,比如线程1某次访问g_cnt时,值为49,下一次再访问时,值可能为50,也可能为51。如果g_cnt值为51,那这个条件永远都不会满足,循环也就永远无法结束。并且线程1中,我们只想获取g_cnt等于50这个状态,没必要每次都去访问,这也会耗费系统资源。

  • 使用条件变量做以下修改
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      #include <mutex>
      #include <condition_variable>
      
      int g_cnt = 0;
      
      // 定义互斥量
      std::mutex g_mutex;
      
      // 定义条件变量
      std::condition_variable g_cond;
      
      bool g_flag = false;
      
      void threadFunc1() {
      	std::unique_lock<std::mutex> lock(g_mutex);
      
      	while (!g_flag) {
      		// 阻塞等待,等待被唤醒
      		g_cond.wait(lock);
      		
      	}
      	std::cout << "threadFunc1 g_cnt: " << g_cnt << std::endl;
      }
      
      void threadFunc2() {
      	while (g_cnt < 100) {
      		g_cnt++;
      		if (g_cnt == 50) {
      			g_flag = true;
      			// 唤醒阻塞的线程
      			g_cond.notify_one();
      		}
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }
      
      int main() {
      	{
      		std::thread th1(threadFunc1);
      		std::thread th2(threadFunc2);
      		th1.join();
      		th2.join();
      
      		std::cout << "g_cnt: " << g_cnt << std::endl;
      	}
      
      	system("pause");
      	return 0;
      }
  • 这样就可以保证线程1的条件肯定可以满足。

线程互斥

  • 控制线程对共享资源的访问。比如写文件时,不能读文件,读文件时,不能写文件。

  • C++11提供了4种互斥锁

    • std::mutex:独占的互斥锁,不能递归使用。
    • std::timed_mutex:带超时的独占互斥锁,不能递归使用。在获取互斥锁资源时增加了超时等待功能。
    • std::recursive_mutex:递归互斥锁,不带超时功能。允许同一线程多次获得互斥锁。
    • std::recursive_timed_mutex:带超时的递归互斥锁。
  • 分析以下这段代码的输出结果
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      #include <mutex>
      
      int g_cnt = 0;
      
      void threadFunc(int num) {
      	for (int i = 0; i < num; i++) {
      		g_cnt++;
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }
      
      
      int main() {
      	{
      		// 线程中执行带参函数
      		std::thread th1(threadFunc, 100);
      		std::thread th2(threadFunc, 100);
      		th1.join();
      		th2.join();
      
      		std::cout << "g_cnt: " << g_cnt << std::endl;
      	}
      
      
      	system("pause");
      	return 0;
      }
  • 我们期望的g_cnt输出结果为200,但实际上g_cnt很大概率不是200而是小于200。这是由于没有对共享资源g_cnt进行加锁保护,这会导致数据竞争。两个线程可能同时访问g_cnt,导致某个线程的++操作被另一个线程覆盖。

  • 对上面代码做下修改,对共享资源g_cnt进行加锁保护。
    *

    c 复制代码
      #include <iostream>
      #include <thread>
      #include <chrono>
      #include <mutex>
      
      int g_cnt = 0;
      
      // 定义互斥量
      std::mutex g_mutex;
      
      void threadFunc(int num) {
      	for (int i = 0; i < num; i++) {
      		// 加锁
      		g_mutex.lock();
      		g_cnt++;
      		// 解锁
      		g_mutex.unlock();
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }
      
      int main() {
      	{
      		// 线程中执行带参函数
      		std::thread th1(threadFunc, 100);
      		std::thread th2(threadFunc, 100);
      		th1.join();
      		th2.join();
      
      		std::cout << "g_cnt: " << g_cnt << std::endl;
      	}
      
      	system("pause");
      	return 0;
      }
  • 加锁后,就可以保证g_cnt的结果为200。

  • 上面代码有这样一种风险。如果加锁后,中途退出而忘记解锁,就会导致死锁现象。
    *

    c 复制代码
      void threadFunc(int num) {
      	for (int i = 0; i < num; i++) {
      		// 加锁
      		g_mutex.lock();
      		g_cnt++;
      		if (i == 50) {
      			break;
      		}
      
      		// 解锁
      		g_mutex.unlock();
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }
  • C++ 11 提供了一种模板类 std::lock_guard,可以简化互斥锁的写法。调用构造时加锁,离开作用域时解锁。不用手动加解锁,大大提高了安全性。

  • 实现代码如下。即便中途退出,也不会出现死锁现象。
    *

    c 复制代码
      void threadFunc(int num) {
      	for (int i = 0; i < num; i++) {
      		// 加锁
      		std::lock_guard<std::mutex> lock(g_mutex);
      		g_cnt++;
      		if (i == 50) {
      			break;
      		}
      		std::this_thread::sleep_for(std::chrono::milliseconds(1));
      	}
      }

参考

相关推荐
小吉在努力敲代码中1 小时前
c++实现B树(下)
开发语言·数据结构·c++·b树·算法
一只小松许️1 小时前
现代C++HTTP框架cinatra
开发语言·c++·http
不爱学英文的码字机器1 小时前
[C++] 智能指针
开发语言·c++·算法
学习前端的小z1 小时前
C语言和C++的常量概念与区别分析
c语言·c++
m0_738054562 小时前
【leetcode】N皇后 回溯法c++
c++·算法·leetcode·回溯
hunandede2 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十四,总结编码过程,从摄像头获得数据后,转成AVFrame,然后再次转成AVPacket,
c++·ffmpeg·音视频
MessiGo3 小时前
C++ 编程基础(5)类与对象 | 5.8、面向对象五大原则
开发语言·c++
sam-zy3 小时前
MFC 鼠标悬停在控件上,显示提示信息
c++·mfc