大纲 C++ 并发编程 线程开始和结束运行 特别注意
进程是否执行完毕,是以主线程是否执行完为标志的。主线程一旦结束,整个进程就会终止,未执行完的子线程也会被操作系统强制结束 。因此,若希望子线程继续运行,那么就必须保持主线程不结束。 值得一提的是,只有当子线程被设计为必须完成的核心任务时,主线程提前结束导致子线程被操作系统强杀,这才属于程序缺陷或不合格的程序设计;若子线程设计为守护线程 或完成辅助性任务,其随主线程终止属于正常行为。 thread 与 join () 的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <chrono> #include <iostream> #include <thread> void func () { std::cout << "sub thread start." << std::endl; for (int i = 1 ; i <= 5 ; ++i) { std::cout << i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::thread t (func) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 main thread start. sub thread start. 1 2 3 4 5 sub thread end. main thread end.
thread 与 detach () 的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <chrono> #include <iostream> #include <thread> void func () { std::cout << "sub thread start." << std::endl; for (int i = 1 ; i <= 5 ; ++i) { std::cout << i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::thread t (func) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下,可以发现在子线程还未执行完,进程(主线程)就已经正常退出:
1 2 3 4 main thread start. main thread end. sub thread start. 1
特别注意
在 C++ 中,子线程调用 detach() 后,std::thread 对象与底层线程的关联会被切断,子线程转为后台运行(即变成守护线程),由 C++ 运行时接管。子线程结束运行后,C++ 运行时会自动清理其资源。特别注意,子线程调用 detach() 后,不能再调用 join(),否则程序会异常退出(崩溃);子线程是否可以调用 join(),可以调用 joinable() 来判断 。
线程创建的其他方式 使用函数对象创建线程 使用函数对象(仿函数)创建线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <chrono> #include <iostream> #include <thread> class MyClass {public : MyClass () { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass& mc) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } void operator () () { std::cout << "sub thread start." << std::endl; for (int i = 1 ; i <= 5 ; ++i) { std::cout << i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } std::cout << "sub thread end." << std::endl; } }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t (mc) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 main thread start. MyClass() MyClass(const MyClass &) // 将主线程中的 MyClass 对象复制进子线程中 sub thread start. 1 2 3 4 5 sub thread end. ~MyClass() // 这里析构的是复制进子线程的 MyClass 对象 main thread end. ~MyClass() // 这里析构的是主线程中定义的 MyClass 对象 mc
特别注意
在上面的代码中,会额外调用一次类对象的拷贝构造函数,导致多拷贝一个类对象。若希望避免多拷贝一份类对象,可以改为使用 std::thread t(std::ref(mc)); 来创建子线程。
使用函数对象创建线程时,应避免引用局部变量
在下面这段代码中,最核心问题是:MyClass 的构造函数接收 int& 引用,该引用绑定到 main 函数的局部变量 i。当子线程执行 detach() 后,主线程可能先于子线程结束运行,导致局部变量 i 被销毁,而子线程中的 m_i 仍持有该悬空引用,后续在 operator() 中访问 m_i 时将产生未定义行为(通常是程序崩溃、数据错乱或看似正常等)。特别注意,在使用函数对象(仿函数)创建线程时,应避免引用局部变量,否则可能会导致悬空引用(发生未定义行为) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <chrono> #include <iostream> #include <thread> class MyClass {public : MyClass (int &i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } void operator () () { std::cout << "sub thread start." << std::endl; for (int i = 1 ; i <= 5 ; ++i) { std::cout << i << ", " << m_i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } std::cout << "sub thread end." << std::endl; } private : int &m_i; }; int main () { std::cout << "main thread start." << std::endl; int i = 100 ; MyClass mc (i) ; std::thread t (mc) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下,可以发现在子线程还未执行完,进程(主线程)就已经正常退出,并且 m_i 成员变量输出的值可能是错误的:
1 2 3 4 5 6 7 8 9 main thread start. MyClass() MyClass(const MyClass &) // 将主线程中的 MyClass 对象复制进子线程中 main thread end. ~MyClass() // 这里析构的是主线程中定义的 MyClass 对象 mc sub thread start. sub thread start. 1, 22088 // 由于在主线程中,MyClass 对象引用了局部变量,因此这里输出的 m_i 成员变量的值可能是错误的;因为主线程先结束运行,局部变量 i 所在的栈内存会跟着提前被释放掉 1, 22088 // 由于在主线程中,MyClass 对象引用了局部变量,因此这里输出的 m_i 成员变量的值可能是错误的;因为主线程先结束运行,局部变量 i 所在的栈内存会跟着提前被释放掉
使用 Lambda 创建线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <chrono> #include <iostream> #include <thread> int main () { std::cout << "main thread start." << std::endl; auto lambda = [] { std::cout << "sub thread start." << std::endl; for (int i = 1 ; i <= 5 ; ++i) { std::cout << i << std::endl; std::this_thread::sleep_for (std::chrono::seconds (1 )); } std::cout << "sub thread end." << std::endl; }; std::thread t (lambda) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 main thread start. sub thread start. 1 2 3 4 5 sub thread end. main thread end.
使用成员函数指针创建线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } void thread_run (MyClass &mc) { std::cout << "sub thread start." << std::endl; mc.m_i = 300 ; std::cout << "sub thread end." << std::endl; } public : int m_i; }; int main () { std::cout << "main thread start." << std::endl; MyClass m_c (200 ) ; std::thread t (&MyClass::thread_run, &m_c, std::ref(m_c)) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 main thread start. MyClass() sub thread start. sub thread end. main thread end. ~MyClass()
线程传递参数的方式 传递临时对象作为线程参数 参数传递实战 这段代码是安全的(正确的),因为主线程必须等待子线程执行结束,然后再往下执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <thread> void func (const int &i, char *buf) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << buf << std::endl; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; int m_i = 1 ; char m_buf[] = "this is a string" ; std::thread t (func, m_i, m_buf) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 main thread start. sub thread start. 1 this is a string sub thread end. main thread end.
要避免的陷阱一 在下面的这段代码中,存在一个严重的悬垂指针问题,这是 C++ 多线程开发中非常经典的错误。核心问题是:参数 m_buf 传递的是指针,但在子线程执行 detach() 后,主线程可能先结束运行,导致局部变量 m_buf 被销毁,最终子线程会访问已释放的内存。
(1) 代码安全的部分:int & 参数
虽然函数签名是 const int &i,但实际上 std::thread 构造函数会对参数做按值拷贝 (除非显式使用 std::ref) 即使 detach() 后主线程销毁了 m_i,子线程持有的也是独立副本,访问是安全的 (2) 代码不安全的部分:char * 参数
m_buf 是局部变量,存储在栈 上std::thread 只是传递了数组首地址(指针值被拷贝),并没有拷贝字符串内容当主线程执行完 return 0 后,m_buf 的内存会被释放 / 覆盖 子线程若此时继续访问 buf,将发生未定义行为 (程序可能会崩溃、输出乱码、看似正常等) 问题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> #include <thread> void func (const int &i, char *buf) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << buf << std::endl; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; int m_i = 100 ; char m_buf[] = "this is a string" ; std::thread t (func, m_i, m_buf) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
解决方案
解决方案一:使用 join() 替代 detach(),让主线程等待子线程结束 1 2 3 4 5 6 7 8 9 10 11 12 13 void func (const int &i, char *buf) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << buf << std::endl; std::cout << "sub thread end." << std::endl; } int main () { int m_i = 100 ; char m_buf[] = "this is a string" ; std::thread t (func, m_i, m_buf) ; t.join (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 void func (const int &i, char *buf) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << buf << std::endl; std::cout << "sub thread end." << std::endl; } int main () { int m_i = 100 ; char m_buf[] = "this is a string" ; std::thread t (func, m_i, std::string(m_buf)) ; t.detach (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void func (const int &i, char *buf) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << buf << std::endl; std::cout << "sub thread end." << std::endl; } int main () { int m_i = 100 ; char m_buf[] = "this is a string" ; auto sp_buf = std::make_shared<char []>(strlen (m_buf) + 1 ); strcpy (sp_buf.get (), m_buf); std::thread t ([m_i, sp_buf]() { func(m_i, sp_buf.get()); }) ; t.detach (); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void func (const int i, const std::string buf) { std::cout << i << std::endl; std::cout << buf << std::endl; } int main () { int m_i = 100 ; char m_buf[] = "this is a string" ; std::thread t (func, m_i, std::string(m_buf)) ; t.detach (); return 0 ; }
总结
本质问题:代码将指向局部栈数组的指针传递给分离线程(子线程),主线程退出后子线程访问已销毁的内存,导致未定义行为。 类似风险:传递局部对象的引用、指向局部对象的指针、迭代器等给调用过 detach() 的子线程(分离线程)。 最佳实践:除非明确知道对象生命周期大于线程生命周期,否则应避免在调用过 detach() 的子线程中使用指针或者引用,尽量传递值类型或智能指针;join() 比 detach() 更安全,能明确控制生命周期。 要避免的陷阱二 在下面的这段代码中,存在一个严重的悬空引用问题,这是 C++ 多线程开发中非常经典的错误。核心问题是:由于 m_c 局部变量通过隐式类型转换构造 MyClass 临时对象的时机不确定,因此代码是不安全的。
问题代码(使用引用 + 隐式类型转换)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <string> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } int get () const { return m_i; } private : int m_i; }; void func (const int &i, const MyClass &mc) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << mc.get () << std::endl; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; int m_i = 100 ; int m_c = 200 ; std::thread t (func, m_i, m_c) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
解决方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <string> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } int get () const { return m_i; } private : int m_i; }; void func (const int &i, const MyClass &mc) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << mc.get () << std::endl; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; int m_i = 100 ; MyClass m_c (200 ) ; std::thread t (func, m_i, m_c) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
解决方案二:使用引用 + 手动构造临时对象(最佳实践 ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <string> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } int get () const { return m_i; } private : int m_i; }; void func (const int &i, const MyClass &mc) { std::cout << "sub thread start." << std::endl; std::cout << i << std::endl; std::cout << mc.get () << std::endl; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; int m_i = 100 ; int m_c = 200 ; std::thread t (func, m_i, MyClass(m_c)) ; t.detach (); std::cout << "main thread end." << std::endl; return 0 ; }
参数传递的最佳实践 建议不要使用 detach(),只使用 join()。这样可以避免因局部变量失效而导致子线程访问非法内存的问题。 如果确实要使用 detach(),应遵守以下规则:传递基础类型(如 int)给线程时,建议使用值传递 ,不要使用引用或指针。 传递类对象(如 MyClass)给线程时:(1) 应避免隐式类型转换 (2) 建议在创建线程的同一行代码中直接构建临时对象,并在线程函数的参数列表中使用引用 来接收该对象,这样做可以避免一次额外的类对象拷贝构造(示例代码如下)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class MyClass {public : MyClass (int i) : m_i (i) { } MyClass (const MyClass &mc) : m_i (mc.m_i) { } int get () const { return m_i; } private : int m_i; }; void func (const int i, const MyClass &mc) { std::cout << i << std::endl; std::cout << mc.get () << std::endl; } int main () { int m_i = 100 ; int m_c = 200 ; std::thread t (func, m_i, MyClass(m_c)) ; t.detach (); return 0 ; }
传递类对象、智能指针作为线程参数 下面这段代码是安全的(因为使用 join() 而不是 detach()),但线程函数里面修改的不是主线程中的 m_c 对象。因为在子线程创建时,已经将主线程中的 m_c 对象拷贝到子线程的内部存储中,这里的 MyClass &mc 引用绑定的是这份拷贝,所以修改不会影响主线程中的 m_c 对象。思考问题:如何才能够在子线程中正确修改主线程中的 mc 对象呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } public : mutable int m_i; }; void func (const MyClass &mc) { std::cout << "sub thread start." << std::endl; mc.m_i = 300 ; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; MyClass m_c (200 ) ; std::thread t (func, m_c) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 main thread start. MyClass() MyClass(const MyClass &) sub thread start. sub thread end. ~MyClass() main thread end. ~MyClass()
解决方案一,使用 std::ref() 强制传递引用作为线程函数参数,避免多拷贝一份类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } public : int m_i; }; void func (MyClass &mc) { std::cout << "sub thread start." << std::endl; mc.m_i = 300 ; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; MyClass m_c (200 ) ; std::thread t (func, std::ref(m_c)) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 main thread start. MyClass() sub thread start. sub thread end. main thread end. ~MyClass()
解决方案二,传递智能指针作为线程函数参数,避免多拷贝一份类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> #include <memory> #include <thread> class MyClass {public : MyClass (int i) : m_i (i) { std::cout << "MyClass()" << std::endl; } MyClass (const MyClass &mc) : m_i (mc.m_i) { std::cout << "MyClass(const MyClass &)" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } public : int m_i; }; void func (std::unique_ptr<MyClass> up) { std::cout << "sub thread start." << std::endl; up->m_i = 300 ; std::cout << "sub thread end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::unique_ptr<MyClass> m_c (new MyClass(200 )) ; std::thread t (func, std::move(m_c)) ; t.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 main thread start. MyClass() sub thread start. sub thread end. ~MyClass() main thread end.
并发与多线程 创建和等待多个线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <chrono> #include <iostream> #include <thread> #include <vector> void func (int num) { std::cout << "sub thread " << num << " start." << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (100 )); std::cout << "sub thread " << num << " end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::vector<std::thread> threads; for (int i = 0 ; i < 10 ; i++) { threads.emplace_back (std::thread (func, i)); } for (std::vector<std::thread>::iterator it = threads.begin (); it != threads.end (); ++it) { it->join (); } std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 main thread start. sub thread 0 start. sub thread 1 start. sub thread 2 start. sub thread 3 start. sub thread 5 start. sub thread 4 start. sub thread 6 start. sub thread 8 start. sub thread 7 start. sub thread 9 start. sub thread 0 end. sub thread 1 end. sub thread 2 end. sub thread 3 end. sub thread 5 end. sub thread 4 end. sub thread 6 end. sub thread 8 end. sub thread 7 end. sub thread 9 end. main thread end.
数据共享问题的分析 只读的数据 关键点
在多线程环境下,只读的共享数据是不存在线程安全问题的,不需要特殊的处理手段,直接读就可以。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <chrono> #include <iostream> #include <thread> #include <vector> std::vector<int > g_v = {1 , 2 , 3 }; void func (int num) { std::cout << "sub thread " << num << " start." << std::endl; std::cout << "sub thread " << num << " read " << g_v[0 ] << ", " << g_v[1 ] << ", " << g_v[2 ] << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (50 )); std::cout << "sub thread " << num << " end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::vector<std::thread> threads; for (int i = 0 ; i < 5 ; i++) { threads.emplace_back (std::thread (func, i)); } for (std::vector<std::thread>::iterator it = threads.begin (); it != threads.end (); ++it) { it->join (); } std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 main thread start. sub thread 0 start. sub thread 0 read 1, 2, 3 sub thread 1 start. sub thread 1 read 1, 2, 3 sub thread 2 start. sub thread 2 read 1, 2, 3 sub thread 3 start. sub thread 3 read 1, 2, 3 sub thread 4 start. sub thread 4 read 1, 2, 3 sub thread 2 end. sub thread 1 end. sub thread 0 end. sub thread 3 end. sub thread 4 end. main thread end.
有读有写 关键点
在多线程环境下,有读有写的共享数据是存在线程安全问题的(比如 3 个线程同时读数据,2 个线程同时写数据),需要特殊的处理手段。 常见的处理手段:针对共享数据,使用互斥锁(如 std::mutex)或读写锁(std::shared_mutex)来保证读读不互斥、读写和写写互斥。 问题代码的简单分析以下代码展示了在多线程环境下,多个线程同时读写共享数据 std::vector<int> g_v 而没有使用任何同步机制,从而产生线程安全问题(数据竞争)。 前 2 个线程执行写操作(修改 vector 内容),后 3 个线程执行读操作(读取 vector 内容)。由于读写同时发生,可能会导致未定义行为(比如读取到不一致的值、程序崩溃等)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <chrono> #include <iostream> #include <thread> #include <vector> std::vector<int > g_v = {1 , 2 , 3 }; void func (int num) { std::cout << "sub thread " << num << " start." << std::endl; if (num < 2 ) { g_v.push_back (num); std::cout << "sub thread " << num << " write value " << num << std::endl; } else { std::cout << "sub thread " << num << " reads: " ; for (int val : g_v) { std::cout << val << " " ; } std::cout << std::endl; } std::this_thread::sleep_for (std::chrono::milliseconds (50 )); std::cout << "sub thread " << num << " end." << std::endl; } int main () { std::cout << "main thread start." << std::endl; std::vector<std::thread> threads; for (int i = 0 ; i < 5 ; i++) { threads.emplace_back (func, i); } for (auto & t : threads) { t.join (); } std::cout << "main thread end." << std::endl; return 0 ; }
存在的线程安全问题 (1) 数据竞争:写线程修改 g_v(例如 push_back 可能会引发 vector 重新分配内存)时,读线程同时遍历 g_v,会导致读线程访问到无效的内存或未更新的状态。 (2) 不一致的读取:读线程可能在写线程修改过程中读取到部分旧数据、部分新数据,甚至因迭代器失效导致程序崩溃。 (3) 未定义行为:根据 C++ 标准,并发读写同一非原子对象且无同步机制时,程序存在未定义行为,可能的表现如下:程序可能正常输出混乱的结果 程序可能抛出异常(如 vector 迭代器失效) 程序可能直接崩溃(段错误等) 共享数据的保护案例 案例背景说明
在网络游戏服务器的设计中,共享数据的保护是一个典型案例:可以创建两个线程,其中一个线程负责收集玩家的命令并将命令数据写入队列,另一个线程则从队列中取出玩家发来的命令,进行解析并执行玩家所需的动作。值得一提的是,在当前业务场景下建议使用生产者消费者模型来实现,并使用 list 容器作为队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <chrono> #include <condition_variable> #include <iostream> #include <list> #include <mutex> #include <thread> class MyClass {public : void inMsgRecvQueue () { for (int i = 0 ; i < 1000 ; i++) { { std::lock_guard<std::mutex> lock (msgRecvQueueMutex) ; msgRecvQueue.push_back (i); } queueNotEmptyCondition.notify_one (); std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } { std::lock_guard<std::mutex> lock (msgRecvQueueMutex) ; stop = true ; } queueNotEmptyCondition.notify_all (); } void outMsgRecvQueue () { while (true ) { std::unique_lock<std::mutex> lock (msgRecvQueueMutex) ; queueNotEmptyCondition.wait (lock, [this ]() { return stop || !msgRecvQueue.empty (); }); if (stop && msgRecvQueue.empty ()) { break ; } int command = msgRecvQueue.front (); msgRecvQueue.pop_front (); lock.unlock (); handleCommand (command); } } private : void handleCommand (int command) { std::cout << "已处理玩家命令: " << command << std::endl; } private : std::list<int > msgRecvQueue; std::mutex msgRecvQueueMutex; std::condition_variable queueNotEmptyCondition; bool stop = false ; }; int main () { MyClass mc; std::thread t_write (&MyClass::inMsgRecvQueue, &mc) ; std::thread t_read (&MyClass::outMsgRecvQueue, &mc) ; t_write.join (); t_read.join (); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 已处理玩家命令: 0 已处理玩家命令: 1 已处理玩家命令: 2 已处理玩家命令: 3 ...... 已处理玩家命令: 996 已处理玩家命令: 997 已处理玩家命令: 998 已处理玩家命令: 999
list 容器和 vector 容器的适用场景
容器 适用场景(效率高) 不适用场景(效率低) list- 任意位置插入 / 删除(给定迭代器) - 从头部 / 尾部操作 - 合并两个列表 - 随机访问(不支持 []) - 查找元素 - 遍历(缓存不友好) vector- 尾部插入 / 删除 - 随机访问(下标访问) - 遍历(缓存友好) - 头部 / 中间插入 / 删除 - 插入导致重新分配内存
提示
C++ 的 list 容器在任意位置频繁插入和删除数据时效率高(尤其是中间位置),因为它只需调整前后节点的指针,不需要移动其他元素。 C++ 的 vector 容器在尾部频繁插入和删除数据时效率高(平均时间复杂度 O(1)),但在头部或中间插入 / 删除数据时效率低,因为需要移动后续所有元素。