C++ :实现多线程编程
多线程
- 1. 多线程基本原理
- 1.1 核心原理
- 线程与进程的关系
- 并发与并行
- 线程的调度与切换
- 共享资源与同步
- 1.2 多线程的优势与挑战
- 2. C++ 标准库多线程(`<thread>`)
- 2.1 thread主要函数
- 1.构造函数(创建线程)
- 2.`join()` 与 `detach()`(线程生命周期管理)
- 3.`joinable()`(检查线程状态)
- 4.`swap()`(交换线程对象)
- 5.`get_id()`(获取线程 ID)
- 6.静态函数 `hardware_concurrency()`
- 7.移动赋值运算符
- 8. 总结
- 2.2 std::mutex线程同步类
- 1.核心成员函数
- **`lock()`**
- **`unlock()`**
- **`try_lock()`**
- 2. 配套的锁管理工具(RAII 机制)
- **`std::lock_guard`(简单自动锁)**
- **`std::unique_lock`(灵活自动锁)**
- 3.其他互斥锁类型
- **`std::recursive_mutex`**
- **`std::timed_mutex`**
- 3. **`std::recursive_timed_mutex`**
- 4. 适用场景总结
- 2.3 基本用法
- 1. 创建线程
- 2. 线程同步:避免数据竞争
- 互斥锁(`std::mutex`)
- 条件变量(`std::condition_variable`)
- 原子变量(`std::atomic`)
- 3. 平台特定的多线程实现
- 3.1 Windows 平台:`CreateThread`(Win32 API)
- 3.2 Linux 平台:`pthread`(POSIX 线程)**
- 4. 多线程编程注意事项**
在 C++ 中实现多线程操作主要依赖于 C++11 及后续标准引入的 <thread>
库,这是跨平台的标准方案。此外,也可以使用平台特定的 API(如 Windows 的 CreateThread
或 Linux 的 pthread
),但推荐优先使用标准库以保证跨平台兼容性。
1. 多线程基本原理
多线程的基本原理是在单个进程中并发多个执行流(线程),这些线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行上下文(如程序计数器、栈空间),从而实现并发执行。
1.1 核心原理
线程与进程的关系
- 进程:操作系统分配资源的基本单位(拥有独立的内存空间、文件句柄等)。
- 线程:进程内的执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的资源,但各自有独立的执行路径。
举例:
一个文本编辑器进程中,可能有3个线程:
- 线程1:处理用户输入(键盘/鼠标)
- 线程2:实时自动保存文件
- 线程3:拼写检查
这些线程共享编辑器的内存(如当前编辑的文本内容),但各自独立运行,用户输入时不会阻塞自动保存。
并发与并行
- 并发:多个线程“交替执行”(宏观上同时推进,微观上CPU在多个线程间快速切换)。
例如:单核CPU中,线程A执行10ms → 切换到线程B执行10ms → 切换回线程A,看起来像同时运行。 - 并行:多个线程“真正同时执行”(需要多核CPU支持)。
例如:双核CPU中,线程A在核心1运行,线程B在核心2运行,物理上同时进行。
多线程的核心价值是通过并发或并行提高程序效率,尤其是在I/O密集型(如网络请求、文件读写)或CPU密集型(如数据计算)场景。
线程的调度与切换
操作系统通过线程调度器决定哪个线程获得CPU时间,调度策略通常有:
- 时间片轮转:每个线程分配固定时间片(如10ms),超时后切换到下一线程。
- 优先级调度:高优先级线程优先获得CPU(如实时任务)。
线程切换时,操作系统会保存当前线程的上下文(程序计数器、寄存器、栈指针等),加载新线程的上下文,这个过程称为上下文切换。切换开销远小于进程切换(因线程共享资源,无需重新分配内存等)。
共享资源与同步
线程共享进程资源(如全局变量、堆内存),但多线程同时操作共享资源会导致数据竞争(结果不一致)。例如:
// 两个线程同时执行:
int count = 0;
void increment() {count++; // 非原子操作(读取→加1→写入)
}
若两个线程同时读取 count=0
,都加1后写入,最终结果可能是 1
而非预期的 2
。
因此需要同步机制保证共享资源的安全访问,如:
- 互斥锁(Mutex):同一时间只允许一个线程访问临界区。
- 信号量(Semaphore):控制同时访问资源的线程数量。
- 条件变量(Condition Variable):实现线程间的等待/通知(如“生产者-消费者”模型)。
1.2 多线程的优势与挑战
-
优势
- 提高资源利用率:CPU在等待I/O(如文件读取)时,可切换到其他线程执行。
- 响应速度提升:UI线程不被耗时操作阻塞(如后台下载时界面仍可交互)。
- 简化程序设计:将不同任务拆分到独立线程,逻辑更清晰。
-
挑战
- 同步复杂性:不当的锁机制可能导致死锁(如线程A持有锁1等待锁2,线程B持有锁2等待锁1)。
- 上下文切换开销:频繁切换线程会消耗CPU资源,过多线程反而降低效率。
- 调试难度:多线程的执行顺序不确定,可能出现偶发的“线程安全”问题。
多线程的核心是在共享资源的基础上实现并发执行,通过操作系统的调度机制实现宏观上的“同时运行”,并通过同步机制解决共享资源的冲突问题。其设计目标是充分利用CPU资源,提升程序的执行效率和响应速度。
2. C++ 标准库多线程(<thread>
)
C++11 引入的 <thread>
库提供了简洁的多线程接口,核心类是 std::thread
,配合同步机制(如互斥锁、条件变量)可实现安全的多线程操作。
2.1 thread主要函数
在 C++11 及后续标准中,std::thread
是多线程编程的核心类,定义于 <thread>
头文件,用于创建和管理线程。以下是其主要成员函数及参数的详细讲解:
1.构造函数(创建线程)
std::thread
的构造函数用于创建线程对象并关联线程函数(或可调用对象),线程在构造完成后立即启动(除非使用默认构造函数)。
- 默认构造函数
std::thread t; // 创建一个空线程对象(不关联任何线程)
功能:创建一个未执行任何任务的线程对象,可后续通过 swap
或移动赋值关联线程。
- 带函数参数的构造函数
template <class F, class... Args>
explicit thread(F&& f, Args&&... args);
f
:线程要执行的函数、lambda 表达式、函数对象等可调用对象。
args...
:传递给函数 f
的参数(可变参数列表)。
功能:创建线程对象,立即执行 f(args...)
。
示例:
// 普通函数
void func(int a, std::string b) { /* ... */ }
std::thread t1(func, 10, "hello"); // 线程执行 func(10, "hello")// lambda 表达式
std::thread t2([](int x) { /* ... */ }, 20); // 线程执行 lambda(20)
- 移动构造函数
thread(thread&& other) noexcept;
参数:other
是另一个 std::thread
对象(右值引用)。
功能:转移线程所有权,other
会变为空线程(不再关联原线程)。
示例:
std::thread t1(func);
std::thread t2 = std::move(t1); // t2 接管 t1 的线程,t1 变为空
2.join()
与 detach()
(线程生命周期管理)
这两个函数是线程管理的核心,必须在线程对象销毁前调用其中一个,否则程序会调用 std::terminate()
终止。
void join();
功能:阻塞当前线程(通常是主线程),等待被调用的线程执行完毕,然后回收线程资源。
注意:
- 只能对可 joinable 的线程调用(即非空线程),否则抛出
std::system_error
。 - 调用后线程对象变为不可 joinable(
joinable()
返回false
)。
示例:
std::thread t(func);
t.join(); // 主线程等待 t 执行完毕
void detach();
功能:将线程与线程对象分离,线程成为“后台线程”,由系统自动回收资源,调用者无需等待。
注意:
- 分离后,线程对象不再关联该线程(
joinable()
返回false
)。 - 必须确保线程访问的资源(如局部变量)生命周期长于线程,否则会导致未定义行为。
示例:
std::thread t(func);
t.detach(); // 线程在后台运行,主线程无需等待
3.joinable()
(检查线程状态)
bool joinable() const noexcept;
- 功能:判断线程对象是否关联一个可执行的线程(即是否可以调用
join()
或detach()
)。 - 返回值:
true
:线程正在运行或等待执行(可 join/detach)。false
:空线程、已 join/detach 的线程。
- 示例:
std::thread t; std::cout << t.joinable() << std::endl; // 输出 0(空线程)std::thread t2(func); std::cout << t2.joinable() << std::endl; // 输出 1(可 join/detach) t2.join(); std::cout << t2.joinable() << std::endl; // 输出 0(已 join)
4.swap()
(交换线程对象)
void swap(thread& other) noexcept;
- 参数:另一个
std::thread
对象other
。 - 功能:交换两个线程对象关联的线程。
- 示例:
std::thread t1(func1); std::thread t2(func2); t1.swap(t2); // t1 现在关联 func2 的线程,t2 关联 func1 的线程
5.get_id()
(获取线程 ID)
std::thread::id get_id() const noexcept;
- 功能:返回线程的唯一标识符(
std::thread::id
类型),空线程返回默认id
。 - 用途:用于区分不同线程,调试或日志记录。
- 示例:
std::thread t(func); std::cout << "线程 ID: " << t.get_id() << std::endl;
6.静态函数 hardware_concurrency()
static unsigned int hardware_concurrency() noexcept;
- 功能:返回当前系统的并发线程数(通常是 CPU 核心数),可用于决定创建线程的数量。
- 返回值:核心数估计值(若无法确定则返回 0)。
- 示例:
std::cout << "CPU 核心数: " << std::thread::hardware_concurrency() << std::endl;
7.移动赋值运算符
thread& operator=(thread&& other) noexcept;
- 功能:将
other
的线程所有权转移给当前对象,若当前对象已关联线程,则先调用terminate()
终止原线程。 - 示例:
std::thread t1(func1); std::thread t2; t2 = std::move(t1); // t2 接管 t1 的线程,t1 变为空
8. 总结
- 注意事项
- 线程对象不可复制:
std::thread
禁用拷贝构造和拷贝赋值,只能移动(move
)。 - 必须调用
join()
或detach()
:线程对象销毁前未调用这两个函数会导致程序异常终止。 - 线程函数的参数传递:参数会被复制到线程的内部存储,若需传递引用,需用
std::ref
包装(确保引用对象生命周期有效)。
void func(int& x) { x++; } int a = 0; std::thread t(func, std::ref(a)); // 传递引用(a 需在 t 执行期间有效) t.join();
- 异常安全:若线程函数抛出未捕获的异常,程序会调用
std::terminate()
终止,需在函数内部捕获异常。
- 线程对象不可复制:
std::thread
的核心函数围绕线程的创建、生命周期管理和状态查询:
- 构造函数用于创建线程并绑定任务。
join()
和detach()
控制线程的等待与分离。joinable()
和get_id()
用于线程状态查询。- 移动语义支持线程所有权的转移。
掌握这些函数是 C++ 多线程编程的基础,配合互斥锁、条件变量等同步机制可实现安全高效的并发操作。
2.2 std::mutex线程同步类
std::mutex
是 C++ 标准库中用于线程同步的核心类,定义于 <mutex>
头文件,用于保护共享资源,避免多个多个线程同时访问导致的数据竞争。其主要函数及适用场景如下:
1.核心成员函数
lock()
void lock();
- 功能:锁定互斥锁。
- 若锁未被其他线程持有,当前线程会获得锁并立即返回。
- 若锁已被其他线程持有,当前线程会阻塞(暂停执行),直到获得锁。
- 注意:
- 同一线程对已锁定的
mutex
再次调用lock()
会导致死锁(线程永久阻塞)。 - 必须确保锁定后最终会解锁,否则其他线程会永久阻塞。
- 同一线程对已锁定的
unlock()
void unlock();
- 功能:解锁互斥锁,释放对锁的所有权,允许其他线程获取该锁。
- 注意:
- 只能对当前线程已锁定的
mutex
调用unlock()
,否则行为未定义(可能崩溃)。 - 通常与
lock()
配对使用,且需确保在所有退出路径(如异常、分支)都能解锁。
- 只能对当前线程已锁定的
try_lock()
bool try_lock();
- 功能:尝试锁定互斥锁(非阻塞)。
- 若锁未被持有,当前线程获得锁并返回
true
。 - 若锁已被持有,立即返回
false
(不阻塞)。
- 若锁未被持有,当前线程获得锁并返回
- 适用场景:需要非阻塞获取锁的场景(如超时等待、尝试性操作)。
2. 配套的锁管理工具(RAII 机制)
直接使用 lock()
和 unlock()
容易因忘记解锁或异常导致死锁,因此通常配合 RAII(资源获取即初始化)风格的工具类使用:
std::lock_guard
(简单自动锁)
template <class Mutex>
class lock_guard;
- 功能:构造时自动调用
mutex.lock()
,析构时自动调用mutex.unlock()
,确保锁的释放。 - 适用场景:临界区范围明确的情况(如一个函数或代码块)。
- 示例:
std::mutex mtx; int shared_data = 0;void increment() {std::lock_guard<std::mutex> lock(mtx); // 构造时锁定shared_data++; // 临界区操作// 析构时自动解锁(无论是否发生异常) }
std::unique_lock
(灵活自动锁)
template <class Mutex>
class unique_lock;
- 功能:比
lock_guard
更灵活,支持手动加锁/解锁、延迟锁定、超时等待等。 - 常用方法:
lock()
:手动锁定。unlock()
:手动解锁。try_lock()
:尝试锁定(非阻塞)。try_lock_for(duration)
:超时等待锁定(阻塞指定时长)。
- 适用场景:
- 临界区需要中途解锁(如先加锁检查条件,不满足则解锁等待)。
- 配合条件变量(
std::condition_variable
)使用(必须用unique_lock
)。
- 示例:
std::mutex mtx; std::condition_variable cv; bool ready = false;void worker() {std::unique_lock<std::mutex> lock(mtx); // 可手动控制的锁cv.wait(lock, []{ return ready; }); // 等待时会临时解锁// 被唤醒后重新获得锁,执行后续操作 }
3.其他互斥锁类型
C++ 标准库还提供了针对特殊场景的互斥锁变种:
std::recursive_mutex
- 特点:允许同一线程多次锁定(递归锁定),需对应次数的解锁。
- 适用场景:递归函数中需要锁定同一资源(但应尽量避免,可能隐藏设计问题)。
std::timed_mutex
- 特点:支持带超时的锁定(
try_lock_for
、try_lock_until
)。 - 适用场景:需要限制锁等待时间的场景,避免永久阻塞。
3. std::recursive_timed_mutex
- 特点:结合
recursive_mutex
和timed_mutex
的功能,支持递归锁定和超时。
4. 适用场景总结
函数/类 | 核心功能 | 适用场景 |
---|---|---|
mutex::lock() | 阻塞锁定 | 确保临界区独占访问 |
mutex::try_lock() | 非阻塞尝试锁定 | 避免线程长时间阻塞(如尝试获取锁失败后做其他事) |
lock_guard | 自动加锁/解锁(RAII) | 简单临界区(范围明确,无需中途解锁) |
unique_lock | 灵活控制的自动锁 | 条件变量、中途解锁、超时等待等复杂场景 |
recursive_mutex | 支持递归锁定 | 递归函数中访问共享资源(谨慎使用) |
timed_mutex | 带超时的锁定 | 需限制锁等待时间的场景 |
-
死锁风险:
- 避免多个线程以不同顺序获取多个锁(如线程 1 先锁 A 再锁 B,线程 2 先锁 B 再锁 A)。
- 可使用
std::lock
函数同时锁定多个锁,避免顺序问题:std::mutex mtx1, mtx2; std::lock(mtx1, mtx2); // 原子操作同时锁定多个锁 std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); // 接管已锁定的锁 std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
-
锁的粒度:
- 锁的范围应尽可能小(只保护必要的共享操作),避免影响并发性能。
- 避免在锁持有期间执行耗时操作(如 I/O、sleep)。
-
线程安全设计:
- 共享资源必须始终被锁保护,避免“漏掉”的访问导致数据竞争。
- 优先使用
lock_guard
而非手动lock()
/unlock()
,减少人为错误。
std::mutex
及其相关工具是 C++ 多线程同步的基础,核心作用是通过互斥访问保护共享资源。实际开发中,推荐优先使用 lock_guard
(简单场景)和 unique_lock
(复杂场景),结合具体需求选择合适的互斥锁类型,同时注意避免死锁和优化锁粒度。
2.3 基本用法
1. 创建线程
通过 std::thread
类创建线程,传入线程函数(或可调用对象)作为参数。
#include <iostream>
#include <thread> // 标准线程库// 线程函数
void thread_func(int id) {std::cout << "线程 " << id << " 启动" << std::endl;// 线程任务...
}int main() {// 创建线程:传入函数和参数std::thread t1(thread_func, 1);std::thread t2(thread_func, 2);// 等待线程执行完毕(必须调用,否则程序可能提前退出)t1.join(); // 阻塞主线程,直到 t1 完成t2.join(); // 阻塞主线程,直到 t2 完成std::cout << "所有线程执行完毕" << std::endl;return 0;
}
- 关键函数:
join()
:主线程等待子线程完成,回收资源。detach()
:将线程与主线程分离(后台运行),无需等待,但需确保线程访问的资源生命周期有效。
2. 线程同步:避免数据竞争
多线程同时操作共享资源会导致数据竞争(未定义行为),需通过同步机制保证线程安全。
互斥锁(std::mutex
)
通过 std::mutex
确保同一时间只有一个线程访问共享资源。
#include <iostream>
#include <thread>
#include <mutex> // 互斥锁std::mutex mtx; // 全局互斥锁
int shared_data = 0;void increment(int id) {for (int i = 0; i < 10000; ++i) {// 加锁:独占访问共享资源std::lock_guard<std::mutex> lock(mtx); // 自动解锁(RAII 机制)shared_data++;// 离开作用域时,lock 自动析构并解锁}
}int main() {std::thread t1(increment, 1);std::thread t2(increment, 2);t1.join();t2.join();std::cout << "最终结果:" << shared_data << std::endl; // 预期 20000return 0;
}
std::lock_guard
:RAII 风格的锁管理,构造时加锁,析构时自动解锁,避免忘记解锁导致死锁。std::unique_lock
:更灵活的锁管理,支持手动加锁/解锁、超时等待等。
条件变量(std::condition_variable
)
用于线程间通信(如等待某个条件满足后再执行)。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool ready = false; // 共享条件void worker() {std::unique_lock<std::mutex> lock(mtx);// 等待条件满足(释放锁并阻塞,被唤醒后重新获取锁)cv.wait(lock, []{ return ready; });std::cout << "worker 线程开始工作" << std::endl;
}int main() {std::thread t(worker);// 主线程准备数据...std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟准备时间{std::lock_guard<std::mutex> lock(mtx);ready = true; // 更新条件}cv.notify_one(); // 唤醒等待的线程t.join();return 0;
}
原子变量(std::atomic
)
用于简单的数值共享变量,无需显式加锁,效率更高。
#include <iostream>
#include <thread>
#include <atomic> // 原子变量std::atomic<int> atomic_data(0); // 原子变量(线程安全)void increment() {for (int i = 0; i < 10000; ++i) {atomic_data++; // 原子操作,无需加锁}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "原子变量结果:" << atomic_data << std::endl; // 预期 20000return 0;
}
3. 平台特定的多线程实现
3.1 Windows 平台:CreateThread
(Win32 API)
#include <iostream>
#include <windows.h>// 线程函数(Win32 要求 __stdcall 调用约定)
DWORD WINAPI ThreadFunc(LPVOID param) {int id = *(int*)param;std::cout << "线程 " << id << " 启动" << std::endl;return 0;
}int main() {int id1 = 1, id2 = 2;// 创建线程HANDLE hThread1 = CreateThread(NULL, // 默认安全属性0, // 默认栈大小ThreadFunc, // 线程函数&id1, // 传递给线程的参数0, // 立即执行NULL // 不需要线程 ID);HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, &id2, 0, NULL);// 等待线程完成WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);// 关闭线程句柄CloseHandle(hThread1);CloseHandle(hThread2);return 0;
}
3.2 Linux 平台:pthread
(POSIX 线程)**
#include <iostream>
#include <pthread.h>// 线程函数
void* ThreadFunc(void* param) {int id = *(int*)param;std::cout << "线程 " << id << " 启动" << std::endl;return NULL;
}int main() {pthread_t tid1, tid2;int id1 = 1, id2 = 2;// 创建线程pthread_create(&tid1, NULL, ThreadFunc, &id1);pthread_create(&tid2, NULL, ThreadFunc, &id2);// 等待线程完成pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
- 编译时需链接
pthread
库:g++ -o thread_demo thread_demo.cpp -lpthread
4. 多线程编程注意事项**
- 数据竞争:多个线程同时读写共享资源时必须同步(用互斥锁、原子变量等)。
- 死锁:避免线程间相互等待对方释放锁(如保持锁的获取顺序一致)。
- 线程生命周期:
detach()
后的线程需确保访问的资源(如局部变量)生命周期有效。 - 性能开销:线程创建/销毁有开销,频繁创建线程可考虑线程池(如 C++17 的
std::async
或第三方库)。 - 异常安全:确保线程函数抛出的异常被捕获,避免程序崩溃。
- 推荐方案:优先使用 C++11 标准库
<thread>
,配合std::mutex
、std::condition_variable
等实现跨平台多线程。 - 核心同步机制:互斥锁(保护共享资源)、条件变量(线程通信)、原子变量(高效简单数值操作)。
- 平台特定场景:仅在需要调用系统特有功能时使用
CreateThread
(Windows)或pthread
(Linux)。
合理使用多线程可充分利用多核 CPU,提升程序性能,但需注意线程安全问题。