本文第一次接触学习多线程,列举刚学习C++多线程时遇到的概念和基础用法。
编译运行需要设置好C++的版本标准为C++11,因为从C++11标准才开始引入标准化的多线程库。
如果使用C++11之前的版本,C++并不支持原生的多线程开发,需要使用第三方库,比如 Qt 的Qthread、第三方库如Linux的<pthread.h>,Windows的<windows.h> 。
如果不想用C++自带的多线程库,也可以选择第三方库。
文章目录
-
- [1. C++多线程核心API](#1. C++多线程核心API)
-
- [1.1 std::thread 基本使用](#1.1 std::thread 基本使用)
- [1.2 线程传参](#1.2 线程传参)
- [1.3 互斥锁(std::mutex):解决数据竞争](#1.3 互斥锁(std::mutex):解决数据竞争)
- [1.4 线程分离(detach)与守护线程](#1.4 线程分离(detach)与守护线程)
- [2. 实战案例](#2. 实战案例)
-
- [2.1 案例1:多线程批量处理任务](#2.1 案例1:多线程批量处理任务)
- [2.2 案例2:多线程读取文件(简单版)](#2.2 案例2:多线程读取文件(简单版))
- [3. 多线程编程常见问题与注意事项](#3. 多线程编程常见问题与注意事项)
-
- [3.1 常见问题](#3.1 常见问题)
- [3.2 入门级注意事项](#3.2 入门级注意事项)
- [4. 总结与进阶](#4. 总结与进阶)
-
- [4.1 总结](#4.1 总结)
- [4.2 进阶方向](#4.2 进阶方向)
- 附:编译运行命令
1. C++多线程核心API
1.1 std::thread 基本使用
std::thread是C++多线程的核心类,用于创建和管理线程。
核心成员函数
| 函数 | 作用 |
|---|---|
join() |
等待其他线程执行完毕(主线程阻塞,直到子线程结束) |
detach() |
分离线程(子线程后台运行,主线程不再管理,也称"守护线程") |
joinable() |
判断线程是否可被join()或detach()(未调用过这两个函数则返回 true ) |
基础示例:创建简单线程
cpp
#include <iostream>
// 必须包含的多线程头文件
#include <thread>
// 线程要执行的函数(普通函数)
void printHello() {
// 子线程的执行逻辑
std::cout << "子线程运行中,线程ID:" << std::this_thread::get_id() << std::endl;
}
int main() {
// 1. 创建线程对象,同时启动子线程(执行printHello函数)
std::thread t(printHello);
// 2. 主线程打印信息
std::cout << "主线程运行中,线程ID:" << std::this_thread::get_id() << std::endl;
// 3. 等待子线程执行完毕(必须!否则主线程退出会导致子线程被强制终止)
if (t.joinable()) {
t.join();
}
return 0;
}
代码解释
std::thread t(printHello):创建线程对象t,并立即启动子线程,执行printHello函数;std::this_thread::get_id():固定语法。获取当前线程的唯一ID(用于区分不同线程);t.join():主线程暂停,等待子线程执行完printHello后,主线程再继续;- 若不调用
join(),主线程会直接退出,操作系统会强制终止未完成的子线程,可能导致程序崩溃或资源泄漏。
1.2 线程传参
std::thread支持给线程函数传递参数,注意参数默认按值拷贝 ,若需传递引用,需用std::ref()。
示例:线程传参
cpp
#include <iostream>
#include <thread>
#include <string>
// 带参数的线程函数
void printMessage(const std::string& msg, int num) {
std::cout << "子线程接收的参数:" << msg << ",数字:" << num << std::endl;
}
int main() {
std::string msg = "Hello Thread";
int num = 100;
// 传递参数:按值传递(msg和num会拷贝一份给子线程)
std::thread t(printMessage, msg, num);
// 等待子线程完成
if (t.joinable()) {
t.join();
}
// 若需传递引用(避免拷贝),需用std::ref():
// std::thread t2(printMessage, std::ref(msg), std::ref(num));
// t2.join();
return 0;
}
1.3 互斥锁(std::mutex):解决数据竞争
数据竞争:多个线程同时访问同一个共享变量,且至少有一个线程是写操作,会导致数据结果错误(这也是多线程最常见的问题)。
std::mutex(互斥锁)用于保护共享资源:同一时间只有一个线程能获取锁,其他线程需等待,直到锁被释放。
核心成员函数
| 函数 | 作用 |
|---|---|
lock() |
获取锁(阻塞,直到拿到锁) |
unlock() |
释放锁(必须和lock配对) |
try_lock() |
尝试获取锁(非阻塞,成功返回true,失败返回false) |
推荐使用
std::lock_guard(RAII封装):自动管理锁的获取和释放,再作用域结束时,会自动解锁,避免忘记unlock()导致死锁。
此外,也可以使用std::unique_lock ,它在 std::lock_guard基础上还可以手动控制锁,更加灵活。
示例:解决多线程累加的竞态条件
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
// 共享变量(多个线程会访问)
int total = 0;
// 互斥锁:保护total
std::mutex mtx;
// 累加函数(子线程执行)
void addNumber(int num) {
for (int i = 0; i < num; ++i) {
// 方式1:手动加锁/解锁(不推荐,易忘unlock)
// mtx.lock();
// total++;
// mtx.unlock();
// 方式2:std::lock_guard(推荐,RAII自动管理)
std::lock_guard<std::mutex> lock(mtx);
total++; // 临界区:只有拿到锁的线程能执行
}
}
int main() {
// 创建2个线程,各自累加10000次
std::thread t1(addNumber, 10000);
std::thread t2(addNumber, 10000);
// 等待两个线程完成
t1.join();
t2.join();
// 正确结果应该是20000(不加锁会小于20000)
std::cout << "最终total值:" << total << std::endl;
return 0;
}
代码解释
- 若不加互斥锁,
total++(本质是load→add→store三步操作)会被两个线程打断,导致最终结果小于20000; std::lock_guard<std::mutex> lock(mtx):创建lock对象时自动调用mtx.lock(),对象销毁时(离开作用域)自动调用mtx.unlock(),即使发生异常也能释放锁;- 临界区 :被互斥锁保护的代码段(如上例中的
total++),同一时间只能有一个线程执行。
1.4 线程分离(detach)与守护线程
detach()将子线程与主线程分离,子线程后台运行(由操作系统接管),主线程无需等待,也无法再通过join()等待。
示例:分离线程
cpp
#include <iostream>
#include <thread>
#include <chrono>
void countDown() {
for (int i = 5; i > 0; --i) {
std::cout << "倒计时:" << i << std::endl;
// 线程休眠1秒(std::chrono是C++11时间库)
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "倒计时结束!" << std::endl;
}
int main() {
std::thread t(countDown);
// 分离线程:子线程后台运行,主线程继续执行
t.detach();
// 主线程打印信息后休眠3秒(若不休眠,主线程直接退出,子线程可能未执行完)
std::cout << "主线程继续执行..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "主线程退出!" << std::endl;
return 0;
}
注意事项
- 分离后的线程不能再调用
join(),否则会崩溃; - 主线程退出时,所有分离的子线程会被强制终止(如上例中主线程3秒后退出,子线程的倒计时只到2就被终止);
- 慎用
detach():若子线程访问主线程的局部变量,主线程退出后变量销毁,子线程会访问野指针。
2. 实战案例
2.1 案例1:多线程批量处理任务
需求:创建4个线程,每个线程处理1个任务(打印任务ID和线程ID)。
cpp
#include <iostream>
#include <thread>
#include <vector>
// 任务函数:处理指定ID的任务
void processTask(int taskId) {
std::cout << "线程ID:" << std::this_thread::get_id()
<< " 处理任务ID:" << taskId << std::endl;
// 模拟任务耗时
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "线程ID:" << std::this_thread::get_id()
<< " 完成任务ID:" << taskId << std::endl;
}
int main() {
// 存储线程对象的容器
std::vector<std::thread> threads;
const int taskCount = 4;
// 创建4个线程
for (int i = 0; i < taskCount; ++i) {
threads.emplace_back(processTask, i); // emplace_back直接创建线程对象
}
// 等待所有线程完成
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
std::cout << "所有任务处理完成!" << std::endl;
return 0;
}
2.2 案例2:多线程读取文件(简单版)
需求:2个线程分别读取2个文件的内容,并打印到控制台。
cpp
#include <iostream>
#include <thread>
#include <fstream>
#include <string>
#include <mutex>
// 互斥锁:保证打印不混乱(多个线程同时打印会导致输出重叠)
std::mutex printMtx;
// 读取文件函数
void readFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
std::lock_guard<std::mutex> lock(printMtx);
std::cout << "无法打开文件:" << filename << std::endl;
return;
}
std::string line;
std::lock_guard<std::mutex> lock(printMtx);
std::cout << "===== 线程" << std::this_thread::get_id() << " 读取文件:" << filename << " =====" << std::endl;
// 逐行读取文件
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
file.close();
}
int main() {
std::thread t1(readFile, "file1.txt");
std::thread t2(readFile, "file2.txt");
t1.join();
t2.join();
return 0;
}
3. 多线程编程常见问题与注意事项
3.1 常见问题
- 死锁 :多个线程互相等待对方释放锁(如线程A拿锁1等锁2,线程B拿锁2等锁1)。
- 避免方式:按固定顺序加锁、使用
std::lock()同时加多个锁、避免锁嵌套过深。
- 避免方式:按固定顺序加锁、使用
- 数据竞争 :未保护共享变量的读写操作。
- 避免方式:用互斥锁保护所有共享变量的读写。
- 野指针 :子线程访问主线程已销毁的局部变量(尤其
detach()的线程)。- 避免方式:传递拷贝而非引用、确保子线程生命周期短于共享变量。
- 过度线程化 :创建过多线程(超过CPU核心数),导致线程切换开销大于并行收益。
- 避免方式:根据CPU核心数设置线程数(可通过
std::thread::hardware_concurrency()获取核心数)。
- 避免方式:根据CPU核心数设置线程数(可通过
3.2 入门级注意事项
- 编译时必须加
-pthread(g++/clang)或链接线程库(MSVC); std::thread对象创建后必须调用join()或detach(),否则析构时会崩溃;- 优先使用
std::lock_guard/std::unique_lock(进阶)而非手动lock()/unlock(); - 线程函数的参数若为临时变量,需确保生命周期覆盖线程执行时间。
4. 总结与进阶
4.1 总结
- C++11 提供
<thread>库实现跨平台多线程,核心类是std::thread; - 多线程的核心问题是数据竞争 ,需用
std::mutex(配合std::lock_guard)保护共享资源; join()等待线程完成,detach()分离线程(慎用);- 多线程编程的关键是"保护共享资源,避免竞态条件"。
4.2 进阶方向
- 条件变量(std::condition_variable):实现线程间的同步(如生产者-消费者模型);
- 原子操作(std::atomic):轻量级的共享变量保护(无需锁,性能更高);
- 线程池:复用线程,避免频繁创建/销毁线程的开销;
- 异步编程(std::async/std::future):更简洁的异步任务处理;
- C++17/C++20 新特性 :如
std::jthread(自动join的线程)、协程等。
附:编译运行命令
Linux/macOS(g++/clang)
bash
# 编译(指定C++11标准+链接线程库)
g++ -std=c++11 -pthread thread_demo.cpp -o thread_demo
# 运行
./thread_demo
Windows(MSVC)
powershell
# 编译(VS开发者命令提示符)
cl /std:c++11 /EHsc thread_demo.cpp
# 运行
thread_demo.exe
持续更新中,,,,,
本次只是初步认识,第二次学习将补充完善,原子量、条件变量、wait 等内容,并比较不同用法区别。