当前位置: 首页 > ai >正文

C++多线程编程深度解析【C++进阶每日一学】

在这里插入图片描述

文章目录

    • 引言
    • 一、 多线程的核心概念
      • 1.1 进程与线程 (Process & Thread)
      • 1.2 并发与并行 (Concurrency & Parallelism)
      • 1.3 竞争条件与临界区 (Race Condition & Critical Section)
    • 二、 `<thread>` 库的基础使用
      • 2.1 创建与启动线程
        • 用法详解: 创建 `std::thread` 对象
      • 2.2 线程的生命周期管理
        • 2.2.1 线程对象的可结合性 (Joinability)
        • 2.2.2 `join()` - 等待线程结束
        • 函数用法: `join()`
        • 2.2.3 `detach()` - 分离线程
        • 函数用法: `detach()`
        • 2.2.4 管理分离的线程
      • 2.3 向线程传递参数
        • 工具用法: `std::ref` 和 `std::cref`
    • 三、 线程同步与数据保护
      • 3.1 互斥量 `std::mutex`
        • 函数用法: `mutex` 成员函数
      • 3.2 使用 `std::lock_guard` 自动管理锁
        • 用法详解: `std::lock_guard`
      • 3.3 `std::unique_lock` - 更灵活的锁管理
        • 用法详解: `std::unique_lock`
      • 3.4 条件变量 `std::condition_variable`
        • 函数用法: `condition_variable`
    • 四、 获取线程的执行结果
      • 4.1 `std::async`, `std::future` 和 `std::promise`
    • 五、 线程中的异常处理
    • 六、 总结


如果觉得本文对您有所帮助,点个赞和关注吧,谢谢,你的支持就是我持续更新的最大动力!!!

引言

随着多核处理器的普及,多线程编程已不再是特定领域的专属技术,而是现代软件开发中压榨硬件性能、提升应用响应能力的关键手段。自C++11标准起,C++语言从标准库层面提供了对多线程的全面支持,使得开发者能够以一种可移植、标准化的方式编写并发程序。本文旨在系统性地梳理C++多线程的核心概念、基础用法、关键同步机制,并对关键功能进行详尽的用法解析,补充了线程返回值、异常处理等高级主题。


一、 多线程的核心概念

在深入代码之前,必须厘清几个核心概念。

1.1 进程与线程 (Process & Thread)

  • 进程 (Process):是操作系统进行资源分配和调度的基本单位。一个进程拥有独立的内存空间、数据栈以及其他系统资源。进程间的通信(IPC)相对复杂且开销较大。
  • 线程 (Thread):是进程内的一个执行单元,是CPU调度的最小单位。一个进程可以包含多个线程,它们共享该进程的内存空间(如代码段、数据段)和资源。因此,线程间的通信更为高效,但也引入了数据同步的挑战。

1.2 并发与并行 (Concurrency & Parallelism)

  • 并发 (Concurrency):指在一个时间段内,多个任务都在向前推进。在单核处理器上,这是通过时间片轮转等方式实现的宏观并行。任务之间是交替执行的。
  • 并行 (Parallelism):指在同一时刻,多个任务同时执行。这需要多核处理器的物理支持,是真正意义上的同时运行。

C++多线程编程的目标,正是利用硬件实现并行,或在逻辑上管理并发任务。

1.3 竞争条件与临界区 (Race Condition & Critical Section)

  • 竞争条件 (Race Condition):当多个线程访问共享数据,并且至少有一个线程会修改数据时,若最终结果取决于线程执行的特定时序,便称之为发生了竞争条件。这通常会导致程序行为不可预测,是并发编程中最常见的错误来源。
  • 临界区 (Critical Section):指程序中访问共享资源(如共享数据)的代码片段。为了避免竞争条件,必须确保在任意时刻,只有一个线程能进入临界区执行。

二、 <thread> 库的基础使用

C++多线程功能主要由头文件 <thread> 提供。

2.1 创建与启动线程

要启动一个新线程,需要创建一个 std::thread 类的实例。在创建该实例时,需要指定新线程应该执行哪个函数,以及传递给该函数的参数。

使用格式:

#include <iostream>
#include <thread>
#include <stdexcept> // for std::exception// 线程入口函数
void thread_function(int arg) {std::cout << "Thread function executing with argument: " << arg << std::endl;
}int main() {try {// 创建一个 thread 对象,新线程立即开始执行 thread_function(100)std::thread my_thread(thread_function, 100); // ... 主线程继续执行 ...my_thread.join(); // 等待子线程结束} catch (const std::system_error& e) {std::cerr << "Caught system_error: " << e.what() << '\n';// 例如:当系统资源不足,无法创建新线程时}return 0;
}
用法详解: 创建 std::thread 对象
std::thread 对象名( 要执行的函数, 函数的第一个参数, 函数的第二个参数, ... );
  • 第一个参数: 一个可调用对象 (Callable Object),例如函数名、函数指针、Lambda 表达式或一个重载了 () 运算符的对象。这指定了新线程的入口点。
  • 后续参数: 传递给上述函数的实参列表。这些参数会被拷贝移动到新线程的内部存储中。
  • 返回值: std::thread 的构造函数没有返回值。
  • 如何判断创建成功: 线程创建是一个可能失败的系统调用。如果操作系统无法创建新线程(例如资源耗尽),std::thread 的构造函数会抛出 std::system_error 异常。因此,将线程创建放在 try-catch 块中是健壮的做法。
  • 作用: 创建一个 thread 对象的同时,会立即启动一个系统级的执行线程。该线程独立于主线程,开始执行你所指定的函数。

2.2 线程的生命周期管理

一个 std::thread 对象在析构时,必须不关联任何正在执行的线程。这意味着,在 thread 对象销毁前,必须明确其关联的后台线程的命运:要么等待它结束 (join),要么让它彻底独立运行 (detach)。

2.2.1 线程对象的可结合性 (Joinability)

一个 std::thread 对象是 “joinable” 的,如果它代表一个正在执行的线程。

  • 默认构造的 std::thread 对象是 non-joinable。
  • join()detach() 过的 std::thread 对象变为 non-joinable。
  • 被移动(std::move)过的 std::thread 对象变为 non-joinable。
2.2.2 join() - 等待线程结束

join() 是一个阻塞操作,用于等待子线程执行完毕,并清理与之相关的资源。

#include <iostream>
#include <thread>
#include <chrono>void worker() {std::cout << "Worker thread started." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作std::cout << "Worker thread finished." << std::endl;
}int main() {std::thread t(worker);std::cout << "Main thread waiting for worker to finish." << std::endl;if (t.joinable()) {t.join(); // 主线程在此处暂停,直到 t 线程执行完毕}std::cout << "Worker thread joined. Main thread finished." << std::endl;return 0;
}
函数用法: join()
thread对象.join();
  • 前置条件: thread 对象必须是 joinable 的。对一个 non-joinable 的 thread 对象调用 join() 会导致程序终止或抛出 std::system_error 异常。
  • 参数: 无。
  • 返回值: 无。
  • 作用:
    1. 阻塞调用 join() 的线程,直到该 thread 对象所代表的线程执行完成。
    2. 回收与已完成线程相关的操作系统资源。
    3. 使 thread 对象本身变为 non-joinable。
  • 一个 thread 对象只能被 join() 一次。
2.2.3 detach() - 分离线程

detach() 会将子线程与 thread 对象分离,让子线程在后台独立运行,成为“守护线程”。

注意: 分离的线程必须自行管理其生命周期。如果主程序退出,所有分离的线程都将被操作系统强制终止,这可能导致资源未释放、文件未关闭、数据损坏等严重问题。因此,detach 的使用场景需非常谨慎。

#include <iostream>
#include <thread>
#include <chrono>void background_task() {std::cout << "Background task started." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));std::cout << "Background task finished." << std::endl;
}int main() {std::thread t(background_task);t.detach(); // 分离线程,主线程继续执行,无需等待std::cout << "Main thread continues execution and will exit soon." << std::endl;// 主线程可能在 background_task 完成前就退出了std::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}
函数用法: detach()
thread对象.detach();
  • 前置条件: thread 对象必须是 joinable 的。
  • 参数: 无。
  • 返回值: 无。
  • 作用: 将执行线程从 thread 对象中分离。分离后,线程继续在后台执行,其生命周期由操作系统管理。thread 对象本身不再与任何执行线程相关联,变为 non-joinable。
2.2.4 管理分离的线程

由于无法 join 分离的线程,我们失去了直接等待它结束的机制。管理它们通常意味着设计一种通信方式,让主线程能够请求它们优雅地退出,并可能等待一个退出的信号。
一个常见的模式是使用原子标志位

#include <atomic>
#include <thread>std::atomic<bool> stop_flag(false);void detached_worker() {while (!stop_flag) {// ... do work ...}// ... cleanup and exit ...
}int main() {std::thread t(detached_worker);t.detach();// ... 主程序运行 ...stop_flag = true; // 在程序退出前,通知分离的线程停止// 注意:这里没有机制确保线程真的停止了,这只是一个请求。return 0;
}

2.3 向线程传递参数

向线程函数传递参数时,默认是值拷贝移动。如需按引用传递,必须使用 std::ref()std::cref() 进行包装。

#include <iostream>
#include <thread>
#include <functional> // for std::ref, std::cref
#include <string>void update_data(int& value, const std::string& msg) { // 第一个参数是引用value = 99;std::cout << msg << std::endl;
}int main() {int data = 10;std::string message = "Hello from thread";// 1. 按引用传递: 使用 std::refstd::thread t1(update_data, std::ref(data), message);t1.join();std::cout << "Final data value: " << data << std::endl; // 输出 99// 2. 按移动传递: 使用 std::movestd::thread t2( [](std::string s){ std::cout << s << std::endl; }, std::move(message) );t2.join();// 此时 message 变量的内容已失效return 0;
}
工具用法: std::refstd::cref
std::ref(变量名)      // 包装成普通引用
std::cref(变量名)     // 包装成 const 引用
  • 作用: 生成一个该变量的“引用包装器”。当把这个包装器作为参数传递给 std::thread 时,它能确保目标函数接收到的是变量的引用,而不是变量值的副本。

三、 线程同步与数据保护

3.1 互斥量 std::mutex

互斥量(Mutex)是保护临界区的基本工具,它像一把锁,确保同一时间只有一个线程能进入被保护的代码区域。

#include <mutex>std::mutex mtx;void critical_section_code() {mtx.lock();   // 获取锁,如果锁被占用则等待// ... 访问共享资源 ...mtx.unlock(); // 释放锁,让其他线程可以获取
}
函数用法: mutex 成员函数
  • mtx.lock(): 尝试锁定互斥量。如果 mtx 未被锁定,则当前线程获得锁并继续执行。如果 mtx 已被其他线程锁定,则当前线程被阻塞,直到获得锁。
  • mtx.unlock(): 释放由当前线程持有的互斥量 mtx 的锁。必须由持有锁的同一个线程调用。
  • mtx.try_lock(): 尝试锁定互斥量,但不阻塞。如果成功获得锁,返回 true。如果锁被其他线程持有,立即返回 false

3.2 使用 std::lock_guard 自动管理锁

手动调用 lock()unlock() 是危险的,因为如果临界区发生异常或提前返回,unlock() 可能不会被调用,导致死锁。std::lock_guard 利用RAII(资源获取即初始化)机制解决了这个问题。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>int counter = 0;
std::mutex mtx;void safe_increment() {for (int i = 0; i < 100000; ++i) {std::lock_guard<std::mutex> lock(mtx); // 创建 lock 对象时,自动锁定 mtxcounter++;} // lock 对象在此处离开作用域,其析构函数会自动调用 mtx.unlock()
}
用法详解: std::lock_guard

std::lock_guard 是一个模板类,专门用于自动管理互斥锁的生命周期。

std::lock_guard<std::mutex> 对象名(互斥量);
  • 作用:
    • 创建时: 在 lock_guard 对象被创建时,它会自动调用传入的互斥量的 lock() 方法。
    • 销毁时: 当 lock_guard 对象离开其作用域时(例如函数结束或抛出异常),它的析构函数会自动调用互斥量的 unlock() 方法。这保证了锁在任何情况下都会被正确释放。

3.3 std::unique_lock - 更灵活的锁管理

std::unique_lock 同样是一个 RAII 风格的锁管理器,但它比 lock_guard 提供了更强的灵活性。

lock_guard 的区别:

  • 所有权: unique_lock 对象拥有它所管理的 mutex 的所有权,这个所有权可以转移。
  • 灵活性: 允许手动调用 lock(), unlock(), try_lock(),并且支持延迟加锁。
std::mutex mtx;
void flexible_lock_usage() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 1. 延迟加锁// ... 做一些不需要锁的操作 ...lock.lock(); // 2. 手动加锁// ... 访问临界区 ...lock.unlock(); // 3. 手动解锁// ... 做一些不需要锁的操作 ...lock.lock(); // 4. 再次加锁// ... 访问临界区 ...
} // 5. 析构时自动解锁(如果此时仍持有锁)
用法详解: std::unique_lock
std::unique_lock<std::mutex> lock(mtx);               // 创建并立即加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 创建但不加锁
  • 主要用途: 当需要对锁进行精细控制时,例如在加锁和解锁之间执行非临界区代码,或者将锁的所有权传递给其他函数。
  • 与条件变量的配合: std::condition_variablewait 函数必须使用 std::unique_lock,因为它需要在等待时临时解锁,并在被唤醒后重新加锁。

3.4 条件变量 std::condition_variable

条件变量(Condition Variable)用于线程间的通信,它允许一个线程等待,直到某个条件变为真,而另一个线程则在条件为真时通知等待的线程。

例子 (生产者-消费者模型):

#include <queue>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;void producer() {for (int i = 0; i < 10; ++i) {{std::lock_guard<std::mutex> lock(mtx);data_queue.push(i);}cv.notify_one(); // 通知一个等待的消费者}{std::lock_guard<std::mutex> lock(mtx);finished = true;}cv.notify_all(); // 通知所有消费者生产结束
}void consumer() {while (true) {std::unique_lock<std::mutex> lock(mtx);// 等待,直到 data_queue 不为空或者生产结束cv.wait(lock, []{ return !data_queue.empty() || finished; });if (!data_queue.empty()) {int data = data_queue.front();data_queue.pop();// ... process data ...} else if (finished) {break; // 队列为空且生产结束,退出循环}}
}
函数用法: condition_variable
// 等待操作
cv.wait(unique_lock<mutex>& lock, Predicate pred);
  • lock: 一个 std::unique_lock 对象。
  • pred: 一个返回布尔值的可调用对象(通常是 Lambda 表达式),代表需要满足的条件。这个谓词是必须的,用以防止伪唤醒 (spurious wakeup)
  • wait 的作用:
    1. 首先检查 pred 条件。
    2. 若条件为 false,则原子地释放锁并将当前线程置于阻塞状态。
    3. 当被 notify 唤醒时,线程会重新获取锁,然后再次检查 pred 条件。只有当条件为 true 时,wait 才会返回,线程继续执行。

  • cv.notify_one(): 唤醒一个正在等待 cv 的线程。
  • cv.notify_all(): 唤醒所有正在等待 cv 的线程。

四、 获取线程的执行结果

std::thread 本身不提供直接获取线程函数返回值的方法。为此,C++标准库提供了 <future> 头文件中的工具。

4.1 std::async, std::futurestd::promise

  • std::future: 代表一个异步操作的“未来”结果。你可以通过 future 对象获取结果(get()),或者等待操作完成。
  • std::promise: 允许你在一个线程中设置值或异常,然后在另一个线程中通过与之关联的 future 对象获取。
  • std::async: 一个高级函数模板,它以异步方式启动一个可调用对象,并返回一个 std::future,该 future 会在任务完成时持有其结果。这是获取线程返回值的最简单方式。

使用 std::async 的例子:

#include <iostream>
#include <future> // 包含 async, future
#include <chrono>int calculate_something(int x) {std::this_thread::sleep_for(std::chrono::seconds(2));return x * x;
}int main() {// 异步启动 calculate_something(10)std::future<int> result_future = std::async(std::launch::async, calculate_something, 10);std::cout << "Main thread is doing other work..." << std::endl;// 从 future 获取结果。如果任务未完成,get() 会阻塞直到完成。int result = result_future.get(); std::cout << "The calculated result is: " << result << std::endl; // 输出 100return 0;
}

五、 线程中的异常处理

如果一个线程函数抛出异常而未在线程内部被捕获,程序的默认行为是调用 std::terminate(),导致整个进程崩溃。

正确的处理方式:

  1. 在线程函数内部使用 try...catch:这是最直接的方法。
  2. 使用 std::promisestd::future 传递异常std::promise 可以捕获异常并存储它。当在 future 对象上调用 get() 时,如果关联的 promise 存储了异常,get() 会重新抛出该异常。std::async 内部自动实现了这个机制。

使用 std::async 自动传递异常的例子:

#include <iostream>
#include <future>
#include <stdexcept>void risky_task() {throw std::runtime_error("An error occurred in the thread!");
}int main() {std::future<void> f = std::async(std::launch::async, risky_task);try {f.get(); // get() 会重新抛出在 risky_task 中抛出的异常} catch (const std::runtime_error& e) {std::cerr << "Caught exception from thread: " << e.what() << std::endl;}return 0;
}

六、 总结

C++11及其后续标准为多线程编程提供了强大且类型安全的工具集。掌握这些工具是现代C++开发者的必备技能。

  • std::thread 是创建和管理线程的基础,但创建可能因系统资源不足而失败(抛出 std::system_error)。
  • 生命周期管理 (join / detach) 是必须履行的责任。thread 对象在析构前必须是 non-joinable 状态,否则程序会终止。
  • std::mutex 与RAII锁(std::lock_guardstd::unique_lock)是保护共享数据、防止竞争条件的核心武器。优先使用 lock_guard,在需要更灵活控制或与条件变量配合时使用 unique_lock
  • std::condition_variable 是实现复杂线程同步与协作(如等待-通知模式)的关键,必须配合 unique_lock 和谓词使用以避免伪唤醒。
  • std::futurestd::async 是从线程安全地获取返回值和传递异常的最佳实践,极大地简化了异步任务的管理。

多线程编程虽然强大,但也带来了复杂性。正确地识别临界区、审慎地选择同步策略、避免死锁,是每一位并发程序员都需要持续实践和深化的课题。

本文到此结束啦,如果觉得对您有所帮助,点个赞和关注吧,谢谢,你的支持就是我持续更新的最大动力!!!

http://www.xdnf.cn/news/18092.html

相关文章:

  • 部署 HAProxy 高可用
  • 将 iPhone 连接到 Windows 11 的完整指南
  • 蛋糕销售管理系统设计与实现
  • MongoDB Windows 系统实战手册:从配置到数据处理入门
  • 【MongoDB】多种聚合操作详解,案例分析
  • Handler以及AsyncTask知识点详解
  • 北斗气象站:能够实现气象数据的实时采集、传输与智能分析
  • 20. 云计算-云服务模型
  • 什么叫做 “可迭代的产品矩阵”?如何落地?​
  • 【前端面试题】JavaScript 核心知识点解析(第二十二题到第六十一题)
  • 使用 Zed + Qwen Code 搭建轻量化 AI 编程 IDE
  • Zookeeper 在 Kafka 中扮演了什么角色?
  • CVPR 2025|英伟达联合牛津大学提出面向3D医学成像的统一分割基础模型
  • 决策树总结
  • CloudBase AI ToolKit + VSCode Copilot:打造高效智能云端开发新体验
  • 在 CentOS 7 上使用 LAMP 架构部署 WordPress
  • CSS:水平垂直居中
  • Java基础(九):Object核心类深度剖析
  • GPT-5在辅助论文写作方面,有哪些进步?
  • 10CL016YF484C8G Altera FPGA Cyclone
  • 千岑智能亮相CIVS2025:国产仿真平台突破技术壁垒,赋能智能汽车产学研融合
  • 【GM3568JHF】FPGA+ARM异构开发板烧录指南
  • 制作全流程对比:侗家灰水粽VS布依族草灰粽的8道工序差异
  • 项目实战——矿物识别系统(利用机器学习从化学元素数据中识别矿物,从数据到分类模型)
  • Linux系统等保三级安全加固执行手册(ReahtCentosKylin)
  • Android中flavor的使用
  • (第十八期)图像标签的三个常用属性:width、height、border
  • 【iOS】锁的原理
  • SIGKDD-2023《Complementary Classifier Induced Partial Label Learning》
  • Unity2022打包安卓报错的奇葩问题