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

C++ 并发编程最佳实践详解

目录

引言

1. 优先选择高层抽象 (std::async)

场景:从子线程计算一个数的平方并返回结果

2. 始终使用 RAII 锁

场景:一个可能抛出异常的函数需要访问共享资源

3. 用条件变量进行同步

场景:一个线程等待另一个线程准备好数据

4. 警惕死锁

场景:从一个银行账户向另一个账户转账,需要同时锁定两个账户

5. 明智地使用原子操作

场景:实现一个多线程共享的计数器

反例:何时不能用 std::atomic?


引言

您在 Canvas 中看到的五条结论是经验丰富的 C++ 开发者总结出的并发编程黄金法则。遵守它们可以帮助您写出更安全、更高效、更易于维护的并发代码。本篇文档将通过具体的代码场景,深入剖析每一条法则背后的“为什么”。

1. 优先选择高层抽象 (std::async)

结论:“尽可能使用 std::asyncstd::future 等工具,它们比手动管理 std::thread 更简单、更安全。”

解读:核心问题是如何从子线程获取计算结果。手动管理 std::thread 来实现这一点,代码会非常繁琐,且容易出错。std::async 将这一切封装好了。

场景:从子线程计算一个数的平方并返回结果

❌ 错误示范:手动管理 std::thread 和共享数据

#include <iostream>
#include <thread>
#include <mutex>void calculate_square(int x, int& result, std::mutex& mtx) {// 模拟耗时计算std::this_thread::sleep_for(std::chrono::milliseconds(500));// 使用互斥锁保护共享变量的写入std::lock_guard<std::mutex> lock(mtx);result = x * x;
}int main() {int result = 0;std::mutex mtx;// 为了获取一个结果,我们不得不引入共享变量和互斥锁std::thread t(calculate_square, 10, std::ref(result), std::ref(mtx));t.join(); // 必须等待线程结束// 手动访问结果std::cout << "手动管理线程,计算结果: " << result << std::endl;return 0;
}

问题:为了传递一个简单的结果,我们被迫引入了共享变量 result 和互斥锁 mtx,代码显得笨重且复杂。

✅ 正确示范:使用 std::async

#include <iostream>
#include <future>int calculate_square_async(int x) {// 模拟耗时计算std::this_thread::sleep_for(std::chrono::milliseconds(500));return x * x;
}int main() {// 一行代码启动异步任务,并获得一个用于取回结果的 futurestd::future<int> result_future = std::async(std::launch::async, calculate_square_async, 10);std::cout << "主线程可以做其他事情..." << std::endl;// 当需要结果时,调用 get()。它会自动等待任务完成。int result = result_future.get();std::cout << "使用 async,计算结果: " << result << std::endl;return 0;
}

优势:代码干净、意图明确。没有手动管理的线程、没有共享变量、没有互斥锁,std::async 完美地表达了“我需要一个后台任务的返回值”这个意图。

2. 始终使用 RAII 锁

结论:“std::scoped_lockstd::lock_guard 是保护共享数据的首选,它们能避免忘记解锁和异常安全问题。”

解读:手动调用 lock()unlock() 最大的风险是,如果在锁定期间发生异常,unlock() 就不会被执行,导致锁永远无法被释放(死锁)。RAII 风格的锁利用了 C++ 的作用域机制,保证锁一定会被释放。

场景:一个可能抛出异常的函数需要访问共享资源

❌ 错误示范:手动 lock()unlock()

#include <iostream>
#include <mutex>
#include <stdexcept>std::mutex mtx;void risky_operation(bool should_throw) {mtx.lock(); // 手动上锁std::cout << "进入临界区..." << std::endl;if (should_throw) {std::cout << "发生错误,即将抛出异常!" << std::endl;throw std::runtime_error("Something went wrong");// mtx.unlock() 将永远不会被执行!}std::cout << "操作成功完成。" << std::endl;mtx.unlock(); // 手动解锁
}int main() {try {risky_operation(true);} catch (const std::exception& e) {std::cerr << "捕获到异常: " << e.what() << std::endl;}// 此时 mtx 仍然是锁定的状态std::cout << "主线程尝试再次获取锁..." << std::endl;if (mtx.try_lock()) { // 尝试获取锁std::cout << "成功获取锁。" << std::endl;mtx.unlock();} else {std::cout << "获取锁失败,程序已死锁!" << std::endl;}return 0;
}

问题:异常导致 unlock() 被跳过,互斥锁被永久锁定。

✅ 正确示范:使用 std::lock_guard (RAII)

#include <iostream>
#include <mutex>
#include <stdexcept>std::mutex mtx_raii;void safe_risky_operation(bool should_throw) {std::lock_guard<std::mutex> lock(mtx_raii); // 构造时自动上锁std::cout << "进入临界区..." << std::endl;if (should_throw) {std::cout << "发生错误,即将抛出异常!" << std::endl;throw std::runtime_error("Something went wrong");}std::cout << "操作成功完成。" << std::endl;
} // 当 lock 对象离开作用域时 (无论是正常结束还是因为异常),析构函数会自动解锁int main() {try {safe_risky_operation(true);} catch (const std::exception& e) {std::cerr << "捕获到异常: " << e.what() << std::endl;}// 此时 mtx_raii 已经被自动解锁std::cout << "主线程尝试再次获取锁..." << std::endl;if (mtx_raii.try_lock()) {std::cout << "成功获取锁。" << std::endl;mtx_raii.unlock();} else {std::cout << "获取锁失败!" << std::endl;}return 0;
}

优势:无论函数如何退出,锁的释放都得到了保证。代码更简洁,也更健壮。

3. 用条件变量进行同步

结论:“避免使用共享标志位进行忙等待,std::condition_variable 更高效。”

解读:“忙等待”(Busy-Waiting)指一个线程在一个循环里不断检查某个条件是否满足。这会持续消耗 CPU 时间,非常低效。条件变量则能让等待的线程进入“睡眠”状态,不消耗 CPU,直到被其他线程唤醒。

场景:一个线程等待另一个线程准备好数据

❌ 错误示范:使用 atomic 标志位进行忙等待

#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>std::atomic<bool> data_ready(false);
std::string shared_data;void data_preparer() {std::this_thread::sleep_for(std::chrono::seconds(1));shared_data = "数据已准备好";data_ready.store(true);
}void data_waiter() {std::cout << "等待者:正在忙等待数据..." << std::endl;while (!data_ready.load()) {// 这个循环会持续空转,浪费 CPUstd::this_thread::sleep_for(std::chrono::milliseconds(10)); // 稍作妥协,但仍是轮询}std::cout << "等待者:收到了数据 -> " << shared_data << std::endl;
}int main() {std::thread t1(data_preparer);std::thread t2(data_waiter);t1.join();t2.join();return 0;
}

问题data_waiter 线程在 while 循环中空转,浪费了大量的 CPU 周期。

✅ 正确示范:使用 std::condition_variable

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>bool data_ready_cv = false;
std::string shared_data_cv;
std::mutex mtx_cv;
std::condition_variable cv;void data_preparer_cv() {std::this_thread::sleep_for(std::chrono::seconds(1));{std::lock_guard<std::mutex> lock(mtx_cv);shared_data_cv = "数据已准备好";data_ready_cv = true;} // 释放锁std::cout << "准备者:已发送通知。" << std::endl;cv.notify_one(); // 通知一个等待的线程
}void data_waiter_cv() {std::cout << "等待者:进入等待状态(睡眠)..." << std::endl;std::unique_lock<std::mutex> lock(mtx_cv);// wait 会让线程睡眠,直到 data_ready_cv 为 true 且被唤醒cv.wait(lock, [] { return data_ready_cv; });std::cout << "等待者:被唤醒并收到了数据 -> " << shared_data_cv << std::endl;
}int main() {std::thread t1(data_preparer_cv);std::thread t2(data_waiter_cv);t1.join();t2.join();return 0;
}

优势data_waiter_cv 线程在调用 cv.wait后会立即进入休眠状态,完全不占用 CPU。直到 data_preparer_cv 调用 cv.notify_one(),它才会被唤醒。这是最高效的等待方式。

4. 警惕死锁

结论:“当需要锁定多个互斥锁时,坚持使用 std::scoped_lock 或保证全局一致的加锁顺序。”

解读:死锁最经典的成因是:两个线程试图以相反的顺序获取两个相同的锁。std::scoped_lock (C++17) 能一次性、安全地锁定多个互斥锁,内部实现了避免死锁的算法。

场景:从一个银行账户向另一个账户转账,需要同时锁定两个账户

❌ 错误示范:按不同顺序锁定互斥锁

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>struct Account {int balance;std::mutex mtx;
};void transfer(Account& from, Account& to, int amount) {// 线程1: transfer(acc1, acc2, 10);// 线程2: transfer(acc2, acc1, 20);std::lock_guard<std::mutex> lock_from(from.mtx); // T1 锁 acc1, T2 锁 acc2std::this_thread::sleep_for(std::chrono::milliseconds(10));std::lock_guard<std::mutex> lock_to(to.mtx);     // T1 等待 acc2, T2 等待 acc1 -> 死锁!from.balance -= amount;to.balance += amount;std::cout << "转账 " << amount << " 完成" << std::endl;
}int main() {Account acc1{100}, acc2{50};// 并发执行两个方向相反的转账std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 20);t1.join();t2.join();std::cout << "acc1 余额: " << acc1.balance << ", acc2 余额: " << acc2.balance << std::endl;return 0; // 程序很可能无法执行到这里
}

问题:两个线程以相反的顺序请求锁,形成了循环等待,导致死锁。

✅ 正确示范:使用 std::scoped_lock

#include <iostream>
#include <thread>
#include <mutex>struct AccountSafe {int balance;std::mutex mtx;
};void safe_transfer(AccountSafe& from, AccountSafe& to, int amount) {// std::scoped_lock 会以一种无死锁的方式同时锁定两个互斥锁std::scoped_lock lock(from.mtx, to.mtx);from.balance -= amount;to.balance += amount;std::cout << "转账 " << amount << " 完成" << std::endl;
}int main() {AccountSafe acc1{100}, acc2{50};std::thread t1(safe_transfer, std::ref(acc1), std::ref(acc2), 10);std::thread t2(safe_transfer, std::ref(acc2), std::ref(acc1), 20);t1.join();t2.join();std::cout << "acc1 余额: " << acc1.balance << ", acc2 余额: " << acc2.balance << std::endl;return 0;
}

优势:无论传入参数的顺序如何,std::scoped_lock 都能保证以一种安全、确定的内部顺序来锁定互斥锁,从而从根本上消除了死锁的可能性。

5. 明智地使用原子操作

结论:“std::atomic 是性能优化的利器,但仅适用于简单的原子操作。复杂的逻辑仍需互斥锁。”

解读:对于像 counter++ 这样非常简单的操作,使用互斥锁的开销(涉及操作系统内核)相对较大。std::atomic 使用特殊的 CPU 指令来完成,速度快得多。但它只能保护对单个变量单个操作

场景:实现一个多线程共享的计数器

❌ 潜在的低效示范:使用互斥锁保护简单计数器

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>long long counter_mutex = 0;
std::mutex mtx_counter;void increment_with_mutex() {for (int i = 0; i < 100000; ++i) {std::lock_guard<std::mutex> lock(mtx_counter);counter_mutex++;}
}
// 这段代码是正确的,但在高竞争环境下可能不是最高效的。

✅ 正确且高效的示范:使用 std::atomic

#include <iostream>
#include <thread>
#include <vector>
#include <atomic>std::atomic<long long> atomic_counter(0);void increment_with_atomic() {for (int i = 0; i < 100000; ++i) {atomic_counter.fetch_add(1); // 这是一个原子操作,比锁更轻量}
}int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(increment_with_atomic);}for (auto& t : threads) {t.join();}std::cout << "原子计数器最终值: " << atomic_counter << std::endl;return 0;
}

优势:对于这种简单的计数场景,std::atomic 的性能远超于使用互斥锁。

反例:何时不能用 std::atomic

当你需要保证多个操作的原子性时,必须用互斥锁。

struct Stats {// 我们希望对 a 和 b 的更新是“捆绑”的int a;int b;
};Stats stats;
std::mutex stats_mtx;void update_stats() {// 错误的做法:使用两个 atomic// std::atomic<int> atomic_a, atomic_b;// atomic_a++; // 这步是原子的// atomic_b--; // 这步也是原子的// 但这两步之间可能被其他线程打断!外部观察者可能看到一个不一致的状态。// 正确的做法:用一个锁保护整个复合操作std::lock_guard<std::mutex> lock(stats_mtx);stats.a++;stats.b--;
}

结论std::atomic 用于单变量的简单操作,std::mutex 用于保护一个代码块(临界区)内的复合操作。

http://www.dtcms.com/a/420222.html

相关文章:

  • 大型网站怎样做优化PHP国内优秀vi设计案例
  • 【论文精读】Few-Shot Object Detection with Attention-RPN and Multi-Relation Detector
  • 山东建设和城乡建设厅注册中心网站驾校视频网站模板
  • 莱芜住房和城乡建设厅网站专业网站建设哪家好
  • 时间轴网站模板仁怀哪儿做网站
  • 装修公司网站asp源码郴州做网站seo
  • 会网站开发想找兼职免费ppt模板下载简约
  • 网站运营与推广电子商务网站建设策略
  • 沈阳网站建设德泰诺谁有qq网站开发资源群
  • 北京公司网站建设定广州英铭网站建设
  • 网站建设详细步骤建建建设网站公司电话
  • 公司网站搭建费用wordpress积分 充值
  • 手机网站布局教程几分钟弄清楚php做网站
  • 网站建设与维护的试卷网站建设里面包含什么语言
  • 【实验报告】华东理工大学随机信号处理实验报告
  • 个人做网站 需要学什么只是陕西网站建设
  • 数据结构入门 (四):双向通行的“高速公路” —— 深入双向循环链表
  • 西安做网站公司玖佰网络如何在google上免费推广
  • 个人域名备案完成了 可以改网站内容吗wordpress个人展示网站6
  • MySQL中SELECT 语句如何 INTO OUTFILE 保存数据到文件
  • 防火墙技术、模型、发展趋势、局限性及安全体系相关分析
  • 上海网站建设公司推荐排名网站建设中英语
  • 电脑从做系统怎么找回以前登录的网站wordpress网站合并
  • 做网站排名多少钱有没有专门做儿童房的网站
  • 菁染料CY5-二硬脂酰基磷脂酰乙醇胺,CY5标记的二硬脂酰基磷脂酰乙醇胺三个核心组成部分
  • 酵母表面展示技术:真核蛋白工程的 “全能工具”,如何重塑生物医学研究?
  • 泸州市住房和城乡建设厅官方网站WordPress破解怎样主题修复
  • redis常见数据结构及其编码方式
  • 双八网站建设深圳定做网站
  • 来个网站奖励自己单页网站怎么制作教程