学懂C++(三十五):深入详解C++ 多线程编程性能优化

server/2024/10/18 8:33:09/

        多线程编程是提高系统性能的有效手段,但在多线程环境下,潜在的问题也随之增加,如死锁、锁争用、上下文切换等。通过优化这些问题,我们可以显著提升多线程程序的执行效率。本文将从避免死锁、减少锁争用和上下文切换三个方面,结合经典示例,深入探讨如何优化 C++ 多线程编程的性能。

1. 避免死锁

1.1 死锁的成因

        死锁是指两个或多个线程在等待对方持有的资源,而彼此互相阻塞,导致程序无法继续执行。产生死锁的四个必要条件为:

  1. 互斥条件:线程在同一时刻只能独占资源。
  2. 请求与保持条件:线程已经持有了一个资源,同时又请求新的资源。
  3. 不可剥夺条件:线程持有的资源在释放前不能被其他线程抢占。
  4. 循环等待条件:存在一个线程等待链,链中的每个线程都在等待下一个线程所持有的资源。

如果满足了这四个条件,程序就可能进入死锁状态。

1.2 避免死锁的方法

常用的避免死锁的方法包括:

  1. 锁的顺序:确保多个线程获取锁的顺序一致,避免循环等待。
  2. 锁的层次:为不同的资源分配优先级,高优先级的锁先获取,低优先级的锁后获取。
  3. 尝试锁机制(std::try_lock:尝试获取多个锁,但如果获取失败,则释放已获取的锁,并重新尝试。

1.3 示例:锁的顺序避免死锁

#include <iostream>
#include <thread>
#include <mutex>std::mutex mutexA;
std::mutex mutexB;void task1() {std::lock_guard<std::mutex> lockA(mutexA); // 先获取锁Astd::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些工作std::lock_guard<std::mutex> lockB(mutexB); // 再获取锁Bstd::cout << "Task 1 executed." << std::endl;
}void task2() {std::lock_guard<std::mutex> lockA(mutexA); // 保持锁顺序一致std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lockB(mutexB); std::cout << "Task 2 executed." << std::endl;
}int main() {std::thread t1(task1);std::thread t2(task2);t1.join();t2.join();return 0;
}

1.4 运行结果分析

通过确保两个线程获取锁的顺序一致(mutexAmutexB 之前),我们避免了死锁的发生。若锁的顺序不一致,例如 task1 先获取 mutexAtask2 先获取 mutexB,则可能会导致死锁。

1.5 核心点总结

  • 死锁的成因:主要由资源竞争和不一致的锁顺序引起。
  • 避免死锁:通过统一的锁顺序、锁层次以及尝试锁机制来避免。

2. 减少锁争用

2.1 锁争用问题

多线程编程中,多个线程试图同时获取相同的锁,可能导致锁争用(contention)。锁争用会导致线程被阻塞,增加线程的等待时间,从而降低并发性能。减少锁争用的核心在于减少锁的粒度,或采用更优化的锁设计。

2.2 减少锁争用的方法

  1. 细粒度锁:将一个大锁分解为多个小锁,只在必要的区域加锁,从而减少线程之间的竞争。
  2. 锁分段(Lock Striping):将资源分段,每个分段独立锁定,线程只需获取所需分段的锁。
  3. 锁分离(Lock Splitting):将不同种类的资源使用不同的锁,以避免不必要的锁共享。

2.3 示例:细粒度锁

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>std::vector<int> sharedData; // 共享数据
std::mutex mtxData; // 数据锁void addData(int id) {std::lock_guard<std::mutex> lock(mtxData); // 对共享数据的操作加锁sharedData.push_back(id);std::cout << "Thread " << id << " added data." << std::endl;
}void processData(int id) {std::lock_guard<std::mutex> lock(mtxData); // 对共享数据的读取加锁if (!sharedData.empty()) {int value = sharedData.back();sharedData.pop_back();std::cout << "Thread " << id << " processed data: " << value << std::endl;}
}int main() {std::vector<std::thread> threads;// 创建多个线程分别添加和处理数据for (int i = 0; i < 5; ++i) {threads.emplace_back(addData, i);threads.emplace_back(processData, i);}for (auto& t : threads) {t.join();}return 0;
}

2.4 运行结果分析

在上述示例中,数据操作(添加和处理)使用细粒度锁。通过对共享数据的独立操作加锁,而不是对整个线程加锁,避免了不必要的锁争用。

2.5 核心点总结

  • 锁争用的危害:锁争用会导致线程阻塞,降低并发性能。
  • 减少锁争用的技巧:采用细粒度锁、锁分段和锁分离等技术,最大限度地减少线程之间的竞争。

3. 上下文切换开销

3.1 上下文切换的代价

上下文切换是指当操作系统从一个线程切换到另一个线程时,需要保存当前线程的状态并加载下一个线程的状态。这种切换涉及到 CPU 寄存器、程序计数器和栈的切换,开销较大,尤其是在频繁的上下文切换下,会严重影响性能。

3.2 减少上下文切换的方法

  1. 优化线程数:避免创建过多的线程,合理配置线程池中的线程数量,使之与硬件 CPU 核心数相匹配。
  2. 减少不必要的阻塞:避免过多的锁竞争和等待操作,减少线程被阻塞的机会。
  3. 使用任务调度:通过调度系统合理分配任务,避免任务切换频繁。

3.3 示例:合理的线程数

#include <iostream>
#include <thread>
#include <vector>void task(int id) {std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟任务std::cout << "Thread " << id << " finished task." << std::endl;
}int main() {const int numThreads = std::thread::hardware_concurrency(); // 获取硬件并发数std::vector<std::thread> threads;// 创建和硬件线程数一致的线程for (int i = 0; i < numThreads; ++i) {threads.emplace_back(task, i);}for (auto& t : threads) {t.join(); // 等待所有线程完成}return 0;
}

3.4 运行结果分析

在该示例中,我们根据硬件的并发能力(std::thread::hardware_concurrency())来创建线程数,以避免线程过多导致频繁的上下文切换。这样做能够确保线程的高效执行,最大化 CPU 的利用率。

3.5 核心点总结

  • 上下文切换的代价:上下文切换涉及到保存和恢复线程状态,频繁的切换会显著增加开销。
  • 减少上下文切换的方法:通过优化线程数、减少阻塞和合理的任务调度,可以有效减少上下文切换的次数,从而提升性能。

4. 总结

在 C++ 多线程编程中,性能优化是至关重要的。本文从三个方面探讨了如何优化多线程程序的性能:

  1. 避免死锁:通过一致的锁顺序、锁层次和尝试锁机制来避免死锁的发生。
  2. 减少锁争用:通过细粒度锁、锁分段和锁分离等技术减少线程间的竞争,提升并发性能。
  3. 减少上下文切换:上下文切换的代价较高,频繁的切换会影响程序性能。通过优化线程数以匹配硬件并发能力,减少线程阻塞和优化任务调度,可以有效减少上下文切换的频率,提升程序的整体性能。

技术精髓与最佳实践

  • 锁顺序与层次:无论是在简单的多线程程序还是复杂的并发环境中,始终保持一致的锁顺序和锁层次是避免死锁的有效策略。
  • 合理使用锁:过度使用大锁虽然简单,但会导致锁争用严重,应用细粒度锁和锁分段技术可以显著提高并发性能。
  • 适当的线程数量:线程数的设置应与硬件实际的并发能力相匹配,过多的线程会导致频繁的上下文切换,从而增加系统开销,合理设置线程数有助于优化性能。

        在实际的多线程程序开发中,除了上述优化策略,程序员还应结合具体场景,进行性能测试和分析,识别瓶颈并进行针对性的优化。通过深入理解并发编程中的关键问题,并应用合适的设计和优化技巧,才能编写出高效、健壮的多线程程序。

上一篇:学懂C++(三十四):深入详解 C++ 高级多线程编程技术中的并发设计模式
下一篇:学懂C++(三十六):深入理解与实现C++进程间通信(IPC)

http://www.ppmy.cn/server/103221.html

相关文章

使用 Dify 和 AI 大模型理解视频内容:Qwen 2 VL 72B

接下来的几篇相关的文章&#xff0c;聊聊使用 Dify 和 AI 大模型理解视频内容。 本篇作为第一篇内容&#xff0c;以昨天出圈的“黑神话悟空制作人采访视频”为例&#xff0c;先来聊聊经常被国外厂商拿来对比的国产模型&#xff1a;千问系列&#xff0c;以及它的内测版。 写在…

实现多goroutine之间的发布和订阅

实现多goroutine之间的发布和订阅 一、实现思路 一个发布者&#xff0c;三个订阅者发布者需要跟每个订阅者之间&#xff0c;都要建立一个chan调用发布方法后&#xff0c;三个订阅者都能收到发布的信息在发布和接收之间&#xff0c;增加暂停&#xff0c;使运行结果更加直观 二…

EMC学习笔记4——传导骚扰发射

传导骚扰发射是最基本的实验项目&#xff0c;主要是检测设备在工作时是否通过电源线产生过强的骚扰发射。 一、传导骚扰发射判断 可以通过两个方面来判断设备是否产生了传导发射&#xff1a; 1.电流的时域波形判断&#xff1a;电流波形与电压的波形不一样。如下图所示&#xf…

大模型Prompt trick:利用大模型同情心提升模型性能

大模型相关目录 大模型&#xff0c;包括部署微调prompt/Agent应用开发、知识库增强、数据库增强、知识图谱增强、自然语言处理、多模态等大模型应用开发内容 从0起步&#xff0c;扬帆起航。 swift与Internvl下的多模态大模型分布式微调指南&#xff08;附代码和数据&#xff…

C# NX二次开发-曲线投影到面上

效果&#xff1a; 代码&#xff1a;根据投影方向投影投影新曲线到面上 var pcb workPart.Features.CreateProjectCurveBuilder(null);pcb.CurveFitData.Tolerance 0.001;pcb.CurveFitData.AngleTolerance 0.05;pcb.SectionToProject.DistanceTolerance 0.001;pcb.SectionT…

Java二十三种设计模式-迭代子模式(16/23)

迭代器模式&#xff1a;顺序访问集合的稳健方式 引言 迭代器模式&#xff08;Iterator Pattern&#xff09;是一种行为型设计模式&#xff0c;它允许顺序访问一个集合对象中的各个元素&#xff0c;而不需要暴露集合的底层表示。 基础知识&#xff0c;java设计模式总体来说设计…

自制深度学习推理框架之Tensor模板类的设计与实现

文章目录 一、Tensort介绍二、Armadillo实现Tensor模板类2.1 tensor类模板2.2 Tensor类的设计2.2.1 矩阵存储顺序2.2.2 Tensor类具体实现 一、Tensort介绍 张量&#xff08;Tensor&#xff09;是一个多维数组的通用化概念&#xff0c;在数学和计算科学中被广泛使用&#xff0c…

如何在 3 分钟内免费在 AWS 上运行 RStudio

欢迎来到雲闪世界。谈到数据分析&#xff0c;我有理由从本地计算机迁移到云端。最突出的是&#xff0c;您可以运行无限数量的机器&#xff0c;而无需拥有或维护它们。此外&#xff0c;您可以在几分钟内根据需要扩大或缩小规模。如果您选择运行 t2.micro 服务器&#xff0c;您可…