提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- C++多线程使用注意点及示例
-
- 一、C++多线程使用核心注意点
-
- [1. 环境与基础要求](#1. 环境与基础要求)
- [2. 线程的创建与销毁](#2. 线程的创建与销毁)
- [3. 数据竞争(竞态条件)](#3. 数据竞争(竞态条件))
- [4. 线程同步机制](#4. 线程同步机制)
- [5. 死锁](#5. 死锁)
- [6. 参数传递与资源生命周期](#6. 参数传递与资源生命周期)
- [7. 异常处理](#7. 异常处理)
- [8. 线程数量](#8. 线程数量)
- 二、示例1:简单多线程使用(基础用法)
- 三、示例2:多线程加锁同步(解决数据竞争)
-
- [1. 未加锁的情况(数据竞争,结果错误)](#1. 未加锁的情况(数据竞争,结果错误))
- [2. 加锁的情况(使用std::mutex+std::lock_guard,结果正确)](#2. 加锁的情况(使用std::mutex+std::lock_guard,结果正确))
- 总结
C++多线程使用注意点及示例
C++11 引入了<thread>库,正式支持原生多线程编程。多线程编程的核心是线程管理 和共享资源同步,以下是关键注意点,以及两个示例(基础多线程用法、多线程加锁同步)。
一、C++多线程使用核心注意点
1. 环境与基础要求
- C++11及以上标准,编译器需支持(如GCC 4.8+、MSVC 2012+、Clang 3.3+)。
- 编译时需链接线程库(如GCC需加
-pthread参数)。
2. 线程的创建与销毁
std::thread对象创建时立即启动线程,需传入可调用对象(函数、lambda、函数对象、类成员函数等)。- 必须对
std::thread对象调用join()(等待线程结束)或detach()(分离线程,后台运行),否则析构时会触发std::terminate()导致程序崩溃。 detach()后的线程由系统接管,无法再控制,需确保线程访问的资源生命周期足够(避免悬垂引用)。
3. 数据竞争(竞态条件)
- 多个线程同时访问共享数据 ,且至少有一个是写操作时,会出现数据竞争,导致未定义行为(结果错误、程序崩溃等)。
- 必须通过同步机制(互斥锁、原子操作、条件变量等)保护共享数据。
4. 线程同步机制
- 互斥锁(std::mutex) :保护临界区,同一时间只有一个线程进入。推荐使用RAII风格的
std::lock_guard(自动加锁/解锁)或std::unique_lock(更灵活,支持手动加锁、超时、与条件变量配合),避免手动解锁遗漏(如异常时)。 - 原子操作(std::atomic):适用于简单数值操作(如计数器),比互斥锁更高效,底层由CPU指令保证原子性。
- 条件变量(std::condition_variable):用于线程间通信(如等待某个条件满足后再执行)。
5. 死锁
- 多个线程互相持有对方需要的锁,导致无限等待。
- 避免方式:按固定顺序加锁、使用
std::lock同时加多个锁、使用C++17的std::scoped_lock(RAII风格多锁)、设置锁超时等。
6. 参数传递与资源生命周期
std::thread传递参数时默认是值拷贝 ,若需传递引用,需用std::ref/std::cref包装。- 线程函数中若访问主线程的局部变量,需确保变量生命周期长于线程(否则会出现悬垂引用)。
7. 异常处理
- 线程函数内的未捕获异常会导致程序终止(
std::terminate()),需在线程内捕获所有异常。
8. 线程数量
- 计算密集型任务:线程数建议等于CPU核心数(避免过多上下文切换)。
- IO密集型任务:线程数可远大于CPU核心数(利用IO等待时间)。
二、示例1:简单多线程使用(基础用法)
该示例展示:线程创建、参数传递(值/引用)、join()等待线程结束、lambda表达式作为线程函数。
cpp
#include <iostream>
#include <thread>
#include <string>
#include <functional> // std::ref
// 普通函数:打印数字
void printNumbers(int n, const std::string& prefix) {
for (int i = 1; i <= n; ++i) {
std::cout << prefix << ": " << i << std::endl;
// 模拟耗时操作(让线程切换更明显)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 类成员函数示例
class MyClass {
public:
void printChars(char c, int times) {
for (int i = 1; i <= times; ++i) {
std::cout << "Char: " << c << " (" << i << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
};
int main() {
// 1. 线程1:调用普通函数,传递值参数
std::thread t1(printNumbers, 5, "Thread1");
// 2. 线程2:调用类成员函数,需传入对象指针/引用 + 函数参数
MyClass obj;
std::thread t2(&MyClass::printChars, &obj, 'A', 4);
// 3. 线程3:使用lambda表达式,传递引用参数(需std::ref)
int num = 3;
std::string str = "Thread3";
std::thread t3([&](int count, const std::string& s) {
for (int i = 1; i <= count; ++i) {
std::cout << s << ": Lambda - " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}, std::ref(num), std::ref(str)); // std::ref传递引用
// 等待所有线程结束(必须调用join,否则析构时崩溃)
t1.join();
t2.join();
t3.join();
std::cout << "所有线程执行完毕!" << std::endl;
return 0;
}
编译运行(GCC):
bash
g++ -std=c++11 thread_basic.cpp -o thread_basic -pthread
./thread_basic
说明:
- 三个线程会交替执行(输出顺序不固定,由操作系统调度)。
- 线程3使用lambda表达式,通过
std::ref传递引用参数,避免值拷贝。
三、示例2:多线程加锁同步(解决数据竞争)
该示例展示:多个线程修改共享变量时的数据竞争问题 ,以及使用std::mutex+std::lock_guard解决同步问题,同时对比原子操作的方案。
1. 未加锁的情况(数据竞争,结果错误)
cpp
#include <iostream>
#include <thread>
#include <vector>
// 共享变量
int g_count = 0;
// 累加函数(未加锁)
void increment(int times) {
for (int i = 0; i < times; ++i) {
// 非原子操作:读取g_count → 加1 → 写回g_count
// 多个线程同时执行时,会出现数据覆盖
g_count++;
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
}
}
int main() {
const int thread_num = 5; // 5个线程
const int times_per_thread = 1000; // 每个线程累加1000次
std::vector<std::thread> threads;
// 创建线程
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(increment, times_per_thread);
}
// 等待线程结束
for (auto& t : threads) {
t.join();
}
// 预期结果:5*1000=5000,但实际结果会小于5000(数据竞争)
std::cout << "最终count值(未加锁):" << g_count << std::endl;
return 0;
}
2. 加锁的情况(使用std::mutex+std::lock_guard,结果正确)
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <mutex> // std::mutex, std::lock_guard
// 共享变量
int g_count = 0;
// 互斥锁:保护g_count
std::mutex g_mutex;
// 累加函数(加锁)
void increment(int times) {
for (int i = 0; i < times; ++i) {
// RAII风格:lock_guard构造时加锁,析构时解锁(即使发生异常也会解锁)
std::lock_guard<std::mutex> lock(g_mutex);
// 临界区:同一时间只有一个线程执行
g_count++;
// 模拟耗时操作(可放在锁外,减少锁的持有时间,提高性能)
// std::this_thread::sleep_for(std::chrono::nanoseconds(1));
}
}
// 可选:原子操作方案(更高效,适用于简单数值操作)
#include <atomic>
std::atomic<int> g_atomic_count(0);
void increment_atomic(int times) {
for (int i = 0; i < times; ++i) {
g_atomic_count++; // 原子操作,无需加锁
std::this_thread::sleep_for(std::chrono::nanoseconds(1));
}
}
int main() {
const int thread_num = 5;
const int times_per_thread = 1000;
std::vector<std::thread> threads;
// 方案1:使用互斥锁
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(increment, times_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "最终count值(加锁):" << g_count << std::endl; // 输出5000
// 方案2:使用原子操作(清空线程容器,重新测试)
threads.clear();
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(increment_atomic, times_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "最终count值(原子操作):" << g_atomic_count << std::endl; // 输出5000
return 0;
}
编译运行(GCC):
bash
g++ -std=c++11 thread_lock.cpp -o thread_lock -pthread
./thread_lock
说明:
std::lock_guard是RAII风格的锁管理,避免了手动调用lock()和unlock()的遗漏问题(如异常时)。- 临界区应尽可能小(如将耗时操作放在锁外),以减少线程阻塞,提高并发性能。
- 简单的数值操作(如计数器)使用
std::atomic更高效,无需互斥锁;复杂的临界区(如多个操作组成的逻辑)使用互斥锁。
总结
C++多线程编程的关键是线程管理 (正确使用join()/detach())和共享资源同步(避免数据竞争,合理使用互斥锁、原子操作、条件变量),同时需注意死锁和资源生命周期问题。上述示例覆盖了基础用法和核心同步场景,可作为多线程编程的入门参考。