当前位置:   article > 正文

C++学习之路(01)—多线程std::thread初探

std::thread

目录

0.前言

1.线程与进程的概念

2.C++11后的多线程

2.1基础使用

2.2小技巧

3.线程安全

3.1互斥锁

3.2原子操作(atomic)


0.前言

由于是学习笔记,就不放在项目经验系列里了。

参考资料:

C++多线程详细讲解_千场托儿索的博客-CSDN博客_c++多线程

https://www.jianshu.com/p/5f0bc41249ad

1.线程与进程的概念

废话不多说。

进程就是程序的实体,比如你打开任务管理器,上面vscode,wegame等等就是应用,一个应用可能包含一个或多个进程,各司其职。一般而言,进程之间是分隔开的,一个崩了不影响其他的,除非进程之间存在通信,那就是多进程编程,但这样有诸如效率低,麻烦等缺点。

而线程就是轻量级的进程,区别在于线程不独立的拥有资源,依赖于创建它的进程而存在,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据。而一个进程至少拥有一个线程。

应用-进程-线程,就像公司-部门-员工吧。

2.C++11后的多线程

2.1基础使用

之前的pthread.h太麻烦,C++11后,就引入了#include<thread>,它在CMakeLists中的引用方式为

  1. find_package(Threads REQUIRED)
  2. target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT})

创建和使用多线程的方式如下

  1. #include <iostream>
  2. #include <thread>
  3. int k=0;
  4. void foo(int n)
  5. {
  6. for(int i=0;i<n;i++)
  7. {k++;}
  8. }
  9. void foo_2(int n)
  10. {
  11. for(int i=0;i<n;i++)
  12. {k++;}
  13. }
  14. int main()
  15. {
  16. std::thread Tr1(foo,100);
  17. std::thread Tr2(foo_2,100);
  18. Tr1.join();
  19. Tr2.join();
  20. std::cout<<k<<std::endl;
  21. }

其中,当创建好std::thread对象时,函数foo和foo_2就已经在执行了,下面的join的意思是,如果当main()函数执行到这里,Tr1和Tr2还在执行,就等等,执行完了才往下走(很合理,线程阻塞,等待结果),detach不推荐使用。

然后这里相当于有两个线程在同时执行foo和foo_2函数的,这就提高了效率。

实际写程序,可能遇到如下报错,原因是函数名可能和某个类的成员函数名重合了,成员函数作为传参需加上类名。

error: no matching function for call to 'std::thread::thread ...

然后这里还有个线程安全的问题,即,上面两个线程同时在修改公共资源k,当你把n调大,比如100000,就会发现k不等于2n。

2.2小技巧

我现在有一段代码,如果我不想专门创建一个函数呢?没关系,可以用匿名函数

std::thread Tr([&](){some codes});

即便这个some codes有千行也可以。

第二个是,我不确定我要创建几个线程,能不能动态决定?

可以,用数组和匿名对象

  1. std::vector<std::thread> TRs;
  2. TRs.reserve(num);
  3. for(int i=0;i<num;i++)
  4. {
  5. TRs.emplace_back(std::thread([&,=i](){some codes})); // 公共资源要么上锁,要么像i一样使用值传递
  6. }
  7. for(int i=0;i<num;i++)
  8. {
  9. TRs[i].join();
  10. }

最后问一下,一个进程能创建多少线程?跟默认预留堆栈空间有关,linux下,大约几百到几千吧。

3.线程安全

3.1互斥锁

可以通过对资源上锁的方式,杜绝同时访问,高级的我还不懂,初级的手动锁和自动锁如下:

①手动上锁解锁

  1. // 手动锁
  2. #include <iostream>
  3. #include <thread>
  4. #include <mutex>
  5. std::mutex mtx;
  6. int k=0;
  7. void foo(int n)
  8. {
  9. mtx.lock();
  10. for(int i=0;i<n;i++)
  11. {k++;}
  12. mtx.unlock();
  13. }
  14. void foo_2(int n)
  15. {
  16. mtx.lock();
  17. for(int i=0;i<n;i++)
  18. {k++;}
  19. mtx.unlock();
  20. }
  21. int main()
  22. {
  23. std::thread Tr1(foo,100);
  24. std::thread Tr2(foo_2,100);
  25. Tr1.join();
  26. Tr2.join();
  27. std::cout<<k<<std::endl;
  28. }

②自动锁

  1. // 自动锁
  2. #include <iostream>
  3. #include <thread>
  4. #include <mutex>
  5. std::mutex mtx;
  6. int k=0;
  7. void foo(int n)
  8. {
  9. std::lock_guard<std::mutex> lock(mtx);
  10. for(int i=0;i<n;i++)
  11. {k++;}
  12. }
  13. void foo_2(int n)
  14. {
  15. std::lock_guard<std::mutex> lock(mtx);
  16. for(int i=0;i<n;i++)
  17. {k++;}
  18. }
  19. int main()
  20. {
  21. std::thread Tr1(foo,100);
  22. std::thread Tr2(foo_2,100);
  23. Tr1.join();
  24. Tr2.join();
  25. std::cout<<k<<std::endl;
  26. }

然后需要注意的是,我们说的同时访问指的是真的同一处,比如你定义一个公共资源结构体,两个线程同时访问同一结构体实例中的不同成员,其实是没有影响的(个人实验,不对请指教)。

3.2原子操作(atomic)

互斥锁虽然保证了安全,但性能开销挺大的,所以C++11引入了原子,即,直接把公共资源设为原子类型,它会自动保证同时只有一个线程访问修改,不需要加锁解锁,代码如下

  1. // 原子
  2. #include <iostream>
  3. #include <thread>
  4. #include <mutex>
  5. #include <atomic>
  6. std::atomic<int> k=0;
  7. void foo(int n)
  8. {
  9. for(int i=0;i<n;i++)
  10. {k++;}
  11. }
  12. void foo_2(int n)
  13. {
  14. for(int i=0;i<n;i++)
  15. {k++;}
  16. }
  17. int main()
  18. {
  19. std::thread Tr1(foo,100);
  20. std::thread Tr2(foo_2,100);
  21. Tr1.join();
  22. Tr2.join();
  23. std::cout<<k<<std::endl;
  24. }

当然原子的玩法有很多,暂时没探究了,总而言之,它的性能开销比互斥锁加锁解锁小,也能保证安全。

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

闽ICP备14008679号