c++线程专题
逐步更新中~~~,参考书籍《C++并发编程实战(第2版)》,不照搬书,只写理解感悟。
引入头文件
#include <thread>
线程启动
std::thread t(my_func);
若需等待线程执行完毕,才继续之后的代码,用join
if (t.joinable()) { t.join(); }
若不等待,可以分离出去(分离出去的线程被称为守护线程daemon thread),此时t.joinable()永远变为false
t.detach();
【线程传参】
线程具有内部存储空间,参数会先复制到该空间,再被线程使用。即使被&修饰。
若要被真正的引用,使用std::ref()
#include <iostream> #include <thread> #include <string> void f(int i, const std::string& s) { std::cout << i << "," << s << std::endl; std::cout << &s << std::endl; } int main() { std::string str = "hello"; std::thread t(f, 3, std::ref(str)); //真正的引用传参 std::cout << &str << std::endl; t.join(); return 0; }
类的非静态成员函数当作线程函数,可以如下方式:
class Camera { public: void do_work(); //类的非静态成员函数 }; void Camera::do_work() { std::cout << "hello" << std::endl; } int main() { Camera* cam=new Camera; std::thread t(&Camera::do_work,cam);//等价于调用cam->do_work() //若do_work(参数1,参数2) //std::thread t(&Camera::do_work, cam, 参数1, 参数2); t.join(); return 0; }
【另外一种传参方式——移动语义】
数据从一个对象转移到另一个对象内部,原对象被“搬空”。即参数只能移动不能复制,这就是移动语义。智能指针常常用到移动语义。
原对象虽然被“搬空”,但是其内存空间依然存在,只是其中的数据没了。参考我的博客第27节c++笔记2(参考learncpp.com) - 夕西行 - 博客园 (cnblogs.com)
【移交线程归属权】
std::thread t1(f1); std::thread t2 = std::move(t1); //函数f1本来在线程t1中执行,现在移交给线程t2执行。t1不再管控。 //t2 = std::thread(f2); //t2已经有管控的任务,不允许再管控其他。 //t1 = std::thread(f2); //t1可以管控f2 if (t1.joinable()) //若t1没有管控任何任务,也就没必要join了,即joinable为false { t1.join(); } if (t2.joinable()) { t2.join(); }
只能移动,不能复制的有:std::thread,std::unique_ptr
void f1(std::thread t); //普通函数,参数为线程对象 void f2(); //线程函数 std::thread t1(f2); f1(std::move(t1)); //具名对象,需显式调用move //f1(std::thread(f2)); //临时对象(非具名对象),自动move //f1(t1); //f1参数类型为std::thread&,可以使用此方式 //std::thread t2 = t1; //错误。只能移动,不能复制。unique_ptr也具有此特性。
【切分任务】
用多线程切分任务,等所有线程结束后才继续执行之后的内容。可以用vector统一管理thread
关于emplace_back与push_back,参考一文轻松搞懂emplace_back与push_back - 知乎 (zhihu.com)
void f1(int i); void g() { std::vector<std::thread> vThreads; for (int i = 0; i < 20; i++) { vThreads.emplace_back(f1, i); //比push_back更强大,emplace_back直接将对象构造在了容器内 } for (auto& entry : vThreads) { entry.join(); } //... }
【线程id】
可以通过std::this_thread::get_id()获取当前线程id,也可以指定id方便区分哪个对象调用了该函数
class Camera { public: void do_work(int cameraID); //类的非静态成员函数 }; void Camera::do_work(int cameraID) { std::cout << cameraID << std::endl; } void g() { Camera* cam1 = new Camera; Camera* cam2 = new Camera; std::vector<std::thread> vThreads; vThreads.emplace_back(&Camera::do_work, cam1, 1);//相机1调用 vThreads.emplace_back(&Camera::do_work, cam2, 2);//相机2调用 for (auto& entry : vThreads) { entry.join(); } //... }
【保护共享数据】
当多线程访问的共享数据不是只读时,就需要被保护。
方法一、用互斥保护
把所有访问共享数据的代码都标记成互斥。注意是所有、所有、所有。包括指针、引用等访问共享数据的方式,如常用的引用传参。
下述函数中第一行都需加上
std::lock_guard<std::mutex> guard(some_mutex);
#include <mutex> int protected_data; std::mutex some_mutex; int get() //读 { return protected_data; } void set(int new_value) //写 { protected_data = new_value; } void add(int& value) //写,操作protected_data { value++; }
防范死锁
当有多个锁时,都在等待对方解锁,一直等不到,就会死锁。可以使用下述方式管理多个锁。
//交换类MyClass的两个对象,为保证互换正确完成,需都加锁。 //一般保证按相同顺序加锁,不会死锁。但有时候无法保证顺序,所以使用下述方式 #include <mutex> class MyClass {}; void swap(MyClass& obj1, MyClass& obj2); class X { private: MyClass m_obj; std::mutex m_mutex; public: X(const MyClass& obj):m_obj(obj){} friend void swap(X& objX1, X& objX2) { if (&objX1 == &objX2) return; std::lock(objX1.m_mutex, objX2.m_mutex); //管理两个锁 std::lock_guard<std::mutex> guard1(objX1.m_mutex, std::adopt_lock); //std::adopt_lock表示已经锁上,guard1依据此接收锁的归属权,不得在构造函数内另行加锁 std::lock_guard<std::mutex> guard2(objX2.m_mutex, std::adopt_lock); swap(objX1.m_obj, objX2.m_obj); } };