当前位置:   article > 正文

C++11的 thread多线程_c++11 thread

c++11 thread

一、C++11的多线程类thread

C++11之前,C++库中没有提供和线程相关的类或者接口,因此在编写多线程程序时,Windows上需要调用CreateThread创建线程,Linux下需要调用clone或者pthread_create来创建线程,但是这样是直接调用了系统相关的API函数,编写的代码,无法做到跨平台编译运行。

C++11之后提供了thread线程类,可以很方便的编写多线程的程序。

std::thread定义了四种构造函数:

  1. thread() noexcept;
  2. template< class Function, class... Args >
  3. explicit thread( Function&& f, Args&&... args );
  4. thread( thread&& other ) noexcept;
  5. thread( const thread& ) = delete;
  • 默认构造函数,创建一个空的 std::thread 执行对象。
  • 初始化构造函数,创建一个 std::thread 对象,可以传入任意函数对象,以及函数参数。在创建thread对象时,std::thread构建函数中的所有参数均会按值并以副本的形式保存成一个tuple对象。
  • 移动构造函数,调用成功之后该线程对象会转移。
  • 拷贝构造函数(被禁用),意味着线程无法拷贝。

随便提一下,当你创建了一个带参(非空的)线程对象时,该线程就会立即执行,不需要显式的调用start或者run 

举个例子,下面展示了四个构造函数的使用

  1. #include <iostream>
  2. #include <utility>
  3. #include <thread>
  4. #include <chrono>
  5. using namespace std;
  6. void f1(int n)
  7. {
  8. for (int i = 0; i < 5; ++i) {
  9. std::cout << "Thread 1 executing\n";
  10. ++n;
  11. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  12. }
  13. };
  14. void f2(int& n)
  15. {
  16. for (int i = 0; i < 5; ++i) {
  17. std::cout << "Thread 2 executing\n";
  18. ++n;
  19. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  20. }
  21. };
  22. class foo
  23. {
  24. public:
  25. void bar()
  26. {
  27. for (int i = 0; i < 5; ++i) {
  28. std::cout << "Thread 3 executing\n";
  29. ++n;
  30. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  31. }
  32. }
  33. int n = 0;
  34. };
  35. class baz
  36. {
  37. public:
  38. void operator()()
  39. {
  40. for (int i = 0; i < 5; ++i) {
  41. std::cout << "Thread 4 executing\n";
  42. ++n;
  43. std::this_thread::sleep_for(std::chrono::milliseconds(10));
  44. }
  45. }
  46. int n = 0;
  47. };
  48. int main()
  49. {
  50. int n = 0;
  51. foo f;
  52. baz b;
  53. std::thread t1([] {
  54. cout << "Hello World from lambda thread." << endl;
  55. }); // 线程会立即执行
  56. std::thread t2(f1, n + 1); // 值传递
  57. std::thread t3(f2, std::ref(n)); //引用传递
  58. std::thread t4(std::move(t3)); // 移动构造函数,t4 is now running f2(). t3 is no longer a thread
  59. std::thread t5(&foo::bar, &f); // 在类的成员函数,t5 runs foo::bar() on object f
  60. std::thread t6(b); // 使用仿函数,t6 runs baz::operator() on a copy of object b
  61. //std::thread t6(std::ref(b));
  62. // 等待线程t2和t4,t5,t6执行完,main线程再继续运行
  63. t2.join();
  64. t4.join();
  65. t5.join();
  66. t6.join();
  67. std::cout << "Final value of n is " << n << '\n';
  68. std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
  69. std::cout << "Final value of b.n (baz::n) is " << b.n << '\n'; // 因为是复制对象,原对象的n不改变
  70. return 0;
  71. }

  注解:

 join:等待子线程,调用线程main处于阻塞模式。直到由 子线程线程执行完毕, join 才返回,join()执行完成之后,底层线程id被设置为0,即joinable()变为false。

joinable:检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。

detach:  将当前线程对象所代表的执行实例与该线程对象分离,不会阻塞当前main线程,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

  1. void foo(int n)
  2. {
  3. std::cout << "start run " << endl;
  4. std::cout << "run " << n << endl;
  5. }
  6. int main()
  7. {
  8. std::thread t1(foo, 1);
  9. if (t1.joinable())
  10. {
  11. std::cout << "thread joinable" << std::endl;
  12. }
  13. std::cout <<"this_thread调用get_id "<< std::this_thread::get_id() << std::endl;
  14. std::cout << "t1调用get_id " << t1.get_id() << std::endl;
  15. t1.detach();
  16. // detach之后,joinable返回false,get_id返回0x0
  17. if (!t1.joinable())
  18. {
  19. std::cout << "t1 can not joinable" << std::endl;
  20. }
  21. std::cout << "t1调用get_id " <<t1.get_id() << std::endl;
  22. return 0;
  23. }

运行结果

二、线程互斥

在多线程环境中运行的代码段,需要考虑是否存在竞态条件。如果存在竞态条件,我们就说该代码段不是线程安全的,是不能直接运行在多线程环境当中。对于这样的代码段,我们经常称之为临界区。临界区又称关键代码段,指的是一小段代码在代码执行前,需要独占一些资源资源,比如访问CPU,打印机等。对于临界区资源,多线程环境下需要保证它以原子操作执行,要保证临界区的原子操作,就需要用到线程间的互斥操作-锁机制。

  1. int g_i = 0;
  2. mutex g_mutex;
  3. void c()
  4. {
  5. for (int i = 0; i < 3; ++i)
  6. {
  7. g_mutex.lock();
  8. cout << std::this_thread::get_id() << " " << g_i++ << endl;
  9. g_mutex.unlock();
  10. }
  11. }
  12. int main()
  13. {
  14. thread t1(c);
  15. thread t2(c);
  16. if (t1.joinable())
  17. t1.join();//调用该函数会阻塞当前线程,直到该t1线程执行完成
  18. if (t2.joinable())
  19. t2.join();
  20. return 0
  21. }

三、基于CAS的原子类 

mutex互斥锁毕竟比较重,对于系统消耗有些大,C++11的thread类库提供了针对简单类型的原子操作类,如std::atomic_int,atomic_long,atomic_bool等,它们值的增减都是基于CAS操作的,既保证了线程安全,效率还非常高

下面代码示例开启10个线程,每个线程对整数增加1000次,保证线程安全的情况下,应该加到10000次,这种情况下,可以用atomic_int来实现,代码示例如下:

  1. #include <iostream>
  2. #include <atomic> // C++11线程库提供的原子类
  3. #include <thread> // C++线程类库的头文件
  4. #include <vector>
  5. // 原子整形,CAS操作保证给sum自增自减的原子操作
  6. std::atomic_int sum = 0;
  7. // 线程函数
  8. void sumTask()
  9. {
  10. // 每个线程给sum加1000次
  11. for (int i = 0; i < 1000; ++i)
  12. {
  13. sum++;
  14. }
  15. };
  16. int main()
  17. {
  18. // 创建10个线程放在容器当中
  19. std::vector<std::thread> vec;
  20. for (int i = 0; i < 10; ++i)
  21. {
  22. vec.push_back(std::thread(sumTask));
  23. }
  24. // 等待线程执行完成
  25. for (int i = 0; i < vec.size(); ++i)
  26. {
  27. vec[i].join();
  28. }
  29. // 所有子线程运行结束,sum的结果每次运行应该都是10000
  30. std::cout << "sum : " << sum << std::endl;
  31. return 0;
  32. }

四、线程的同步通信

多线程在运行过程中,各个线程都是随着操作系统的调度算法,占用CPU时间片来执行指令做事情,每个线程的运行完全没有顺序可言。但是在某些应用场景下,一个线程需要等待另外一个线程的运行结果,才能继续往下执行,这就需要涉及线程之间的同步通信机制。

线程间同步通信最典型的例子就是生产者-消费者模型,生产者线程生产出产品以后,会通知消费者线程去消费产品;如果消费者线程去消费产品,发现还没有产品生产出来,它需要通知生产者线程赶快生产产品,等生产者线程生产出产品以后,消费者线程才能继续往下执行。

C++11 线程库提供的条件变量condition_variable,就是Linux平台下的Condition Variable机制,用于解决线程间的同步通信问题,下面通过代码演示一个生产者-消费者线程模型,仔细分析代码:

  1. #include <iostream> // std::cout
  2. #include <thread> // std::thread
  3. #include <mutex> // std::mutex, std::unique_lock
  4. #include <condition_variable> // std::condition_variable
  5. #include <vector>
  6. // 定义互斥锁(条件变量需要和互斥锁一起使用)
  7. std::mutex mtx;
  8. // 定义条件变量(用来做线程间的同步通信)
  9. std::condition_variable cv;
  10. // 定义vector容器,作为生产者和消费者共享的容器
  11. std::vector<int> vec;
  12. // 生产者线程函数
  13. void producer()
  14. {
  15. // 生产者每生产一个,就通知消费者消费一个
  16. for (int i = 1; i <= 10; ++i)
  17. {
  18. // 获取mtx互斥锁资源
  19. std::unique_lock<std::mutex> lock(mtx);
  20. // 如果容器不为空,代表还有产品未消费,等待消费者线程消费完,再生产
  21. while (!vec.empty())
  22. {
  23. // 判断容器不为空,进入等待条件变量的状态,释放mtx锁,
  24. // 让消费者线程抢到锁能够去消费产品
  25. cv.wait(lock);
  26. }
  27. vec.push_back(i); // 表示生产者生产的产品序号i
  28. std::cout << "producer生产产品:" << i << std::endl;
  29. /*
  30. 生产者线程生产完产品,通知等待在cv条件变量上的消费者线程,
  31. 可以开始消费产品了,然后释放锁mtx
  32. */
  33. cv.notify_all();
  34. // 生产一个产品,睡眠100ms
  35. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  36. }
  37. }
  38. // 消费者线程函数
  39. void consumer()
  40. {
  41. // 消费者每消费一个,就通知生产者生产一个
  42. for (int i = 1; i <= 10; ++i)
  43. {
  44. // 获取mtx互斥锁资源
  45. std::unique_lock<std::mutex> lock(mtx);
  46. // 如果容器为空,代表还有没有产品可消费,等待生产者生产,再消费
  47. while (vec.empty())
  48. {
  49. // 判断容器为空,进入等待条件变量的状态,释放mtx锁,
  50. // 让生产者线程抢到锁能够去生产产品
  51. cv.wait(lock);
  52. }
  53. int data = vec.back(); // 表示消费者消费的产品序号i
  54. vec.pop_back();
  55. std::cout << "consumer消费产品:" << data << std::endl;
  56. /*
  57. 消费者消费完产品,通知等待在cv条件变量上的生产者线程,
  58. 可以开始生产产品了,然后释放锁mtx
  59. */
  60. cv.notify_all();
  61. // 消费一个产品,睡眠100ms
  62. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  63. }
  64. }
  65. int main()
  66. {
  67. // 创建生产者和消费者线程
  68. std::thread t1(producer);
  69. std::thread t2(consumer);
  70. // main主线程等待所有子线程执行完
  71. t1.join();
  72. t2.join();
  73. return 0;
  74. }

参考:

C++11 - thread多线程编程,线程互斥和同步通信,死锁问题分析解决_大秦坑王的专栏-CSDN博客

《深入应用C++11》笔记-线程std::thread_WizardtoH-CSDN博客

std::thread详解 - _yanghh - 博客园

多线程 - 从 pthread 转换到 std::thread_个人文章 - SegmentFault 思否

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/153589
推荐阅读
相关标签
  

闽ICP备14008679号