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

多线程编程中的重要概念

并行与并发​

并发​

在单核处理器环境下,操作系统通过时间片轮转的方式,让每个任务轮流占用 CPU 时间片,表面上多个任务同时在执行,实际上每个时刻只有一个任务在 CPU 上运行,任务之间交替执行,这种交替执行的现象被称为并发。比如,在单核电脑上同时运行音乐播放器和浏览器,音乐播放器播放音乐和浏览器加载网页的任务交替使用 CPU 时间片 ,给用户一种同时进行的错觉。​

并行​

并行是指在多个 CPU 核心上,多个任务真正意义上的同时执行。当系统具备多个 CPU 核心时,不同的任务可以被分配到不同的核心上,各自独立执行,互不干扰。例如,在多核服务器上,一个核心处理数据库查询任务,另一个核心处理用户请求任务,这些任务能够真正地同时进行。​

在实际应用场景中,由于在并发场景下,单个核上的任务执行也具有类似并行的交替特性,所以常用 “并行” 一词来泛指并行和并发两种情况。​

多线程优势:适用场景分析​

多线程并非在所有场景下都能带来性能提升,其适用性取决于具体的应用场景类型,主要分为IO 密集型和CPU(计算)密集型。​

多核环境​

在多核处理器环境下,IO 密集型和 CPU 密集型任务都适合使用多线程技术。不过,IO 密集型任务更加适合多线程,原因在于:IO 密集型任务在执行过程中,大量时间处于等待外部 IO 设备(如磁盘读写、网络请求等)响应的状态,此时 CPU 处于空闲状态,多线程可以让 CPU 在等待 IO 操作完成的时间里去处理其他任务,充分利用 CPU 资源,提高系统整体效率。而对于 CPU 密集型任务,多线程可以将计算任务分配到多个核心上并行处理,加快任务完成速度。​

单核环境​

在单核处理器环境下,情况则有所不同。IO 密集型任务依然适合多线程,因为多线程可以在某个任务等待 IO 的过程中,切换到其他任务继续执行,避免 CPU 闲置。但 CPU 密集型任务并不适合多线程,这是因为线程调度存在额外开销,频繁的线程上下文切换会占用大量 CPU 时间。线程上下文切换是指保存当前线程的执行状态(如程序计数器、寄存器值等),然后加载另一个线程的执行状态,以便另一个线程能够继续执行,这一过程会消耗 CPU 资源,对于 CPU 密集型任务反而会降低执行效率。​

IO 密集型与 CPU(计算)密集型任务​

IO 密集型任务​

IO 密集型任务主要涉及大量的 IO 操作,如文件读写、网络数据传输、数据库查询等。这类任务的特点是,CPU 处理时间较短,大部分时间都在等待外部 IO 设备完成操作。例如,在一个网络爬虫程序中,从网页下载数据时,需要等待网络响应,在等待过程中 CPU 处于空闲状态,此时利用多线程可以同时发起多个下载请求,充分利用网络带宽和 CPU 资源。​

CPU(计算)密集型任务​

CPU(计算)密集型任务侧重于大量的计算工作,如数据加密、数据分析等。这类任务几乎全程占用 CPU 资源,CPU 始终处于忙碌状态。例如,使用蒙特卡罗方法计算圆周率,整个过程主要依靠 CPU 进行大量的数值计算,几乎没有 IO 操作,此时(尤其单核环境下)过多的线程不仅不能提升效率,反而会因为线程上下文切换带来额外开销。​

线程创建与数量限制​

在实际开发中,为了完成任务,不能无限制地创建线程,线程并非越多越好。以网络库工作线程为例,通常按照 CPU 核心数量来确定线程数量,这是基于以下几个原因:​

  1. 线程创建和销毁开销大:线程的创建和销毁涉及到用户空间向内核空间的切换。创建线程时,系统需要为其创建 PCB(进程控制块,在 Linux 中为 task_struct),分配线程内核栈,设置页目录 / 页表以及描述地址空间的数据结构(如 vm_struct、vm_area_struct 等),完成后再切换回用户空间 。销毁线程时同样需要进行复杂的清理工作。频繁地创建和销毁线程会消耗大量系统资源,因此在服务运行过程中,不宜实时创建和销毁线程。​
  2. 线程栈占用内存多:每个线程都有自己独立的栈空间,用于存储局部变量、函数调用信息等。在大多数系统中,一个线程栈默认大小为 8MB。如果创建大量线程,这些线程栈会占用大量内存,可能导致系统内存不足。​
  3. 线程上下文切换开销大:如前文所述,大量线程会导致频繁的上下文切换,这会占用大量 CPU 时间,降低 CPU 的利用率,使得任务执行效率下降。​
  4. 系统负载过高:大量线程同时唤醒时,会使系统经常出现锯齿状负载或瞬间负载过大的情况,严重时可能导致系统宕机。​

Fixed 模式和 Cached 模式线程池​

为了更好地管理线程,避免上述问题,线程池技术应运而生。常见的线程池模式有 Fixed 模式和 Cached 模式:​

Fixed 模式线程池​

Fixed 模式线程池拥有固定数量的线程,线程池创建后,线程数量不会发生变化。这些线程会一直存在于线程池中,等待任务分配。当有任务提交时,空闲的线程会处理任务;如果所有线程都在忙碌,新任务会被放入任务队列等待。这种模式适用于任务数量可预测,且需要保证线程数量稳定的场景。例如,在一个订单处理系统中,订单处理任务的数量相对稳定,使用 Fixed 模式线程池可以合理分配线程资源,保证订单能够有序处理。​

之前的文章中使用的就是Fixed 模式线程池​RK3566/RK3588 YoloV5部署(四) yolov5多线程部署_yolov5如何用部署在rk3568上-CSDN博客

Cached 模式线程池​

Cached 模式线程池在有任务提交时,如果当前线程池中有空闲线程,则复用空闲线程处理任务;如果没有空闲线程,则创建新的线程处理任务。当线程空闲时间超过一定阈值时,线程会被销毁。这种模式适用于任务数量波动较大,执行时间较短的场景。例如,在一个 Web 服务器中,处理用户的 HTTP 请求,请求数量不固定且处理时间较短,Cached 模式线程池可以灵活地根据任务数量调整线程数量,提高资源利用率。

线程同步与通信​

竞态条件与临界区​

一段代码能否在多线程环境下正确执行,关键在于这段代码是否存在竞态条件。竞态条件是指代码片段在多线程环境下执行时,由于线程的调度顺序不同,而得到不同的运算结果。存在竞态条件的代码段被称为临界区代码段,对于临界区代码段,需要保证其原子操作,以确保数据的一致性和正确性。如果一段代码不存在竞态条件,则称其为可重入代码;反之,则为不可重入代码。​

线程互斥​

在多线程环境下,当多个线程同时访问共享资源时,可能会出现数据不一致的问题,为了解决这个问题,需要使用线程互斥机制。常见的线程互斥手段有互斥锁 mutex 和原子变量 atomic。​

互斥锁 mutex​

互斥锁是一种最基本的同步工具,它保证同一时间只有一个线程能够访问被保护的共享资源。当一个线程获取到互斥锁后,其他线程如果想要访问该资源,必须等待锁的释放。例如:

#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;
int shared_variable = 0;void increment() {for (int i = 0; i < 10000; ++i) {std::lock_guard<std::mutex> guard(mtx);shared_variable++;}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_variable << std::endl;return 0;
}

在上述代码中,通过 std::lock_guardstd::mutex guard (mtx) 语句获取互斥锁,保证了对 shared_variable 的操作是原子性的,避免了数据竞争。​

原子变量 atomic​

原子变量是一种特殊类型的变量,其操作是原子的,即不会被线程调度打断。例如,std::atomic类型的变量,对其进行的加减等操作都是原子操作,不需要额外的锁保护。

#include <iostream>
#include <thread>
#include <atomic>std::atomic<int> shared_atomic_variable(0);void atomic_increment() {for (int i = 0; i < 10000; ++i) {shared_atomic_variable++;}
}int main() {std::thread t1(atomic_increment);std::thread t2(atomic_increment);t1.join();t2.join();std::cout << "Final atomic value: " << shared_atomic_variable << std::endl;return 0;
}

线程通信:生产者消费者模型​

线程通信是多线程编程中的重要环节,生产者消费者模型是一种经典的线程通信方式。在生产者消费者模型中,生产者线程负责生成数据并放入缓冲区,消费者线程从缓冲区中取出数据进行处理。为了保证生产者和消费者之间的协调工作,需要使用条件变量等同步机制。​

条件变量可以让线程在满足特定条件之前进入等待状态,当条件满足时,再唤醒等待的线程。在使用条件变量时需要注意,它会改变当前线程状态为等待,然后释放锁(因此不能使用 lock_guard,因为其释放锁只能在析构函数释放)。以下是一个简单的生产者消费者模型示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::mutex mtx;
std::condition_variable cv;
std::queue<int> buffer;
const int buffer_size = 5;// 生产者线程
void producer() {int data = 0;while (true) {{std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return buffer.size() < buffer_size; });buffer.push(data++);std::cout << "Produced: " << buffer.back() << std::endl;}cv.notify_one();}
}// 消费者线程
void consumer() {while (true) {{std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return!buffer.empty(); });int data = buffer.front();buffer.pop();std::cout << "Consumed: " << data << std::endl;}cv.notify_one();}
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

在上述代码中,通过条件变量 cv 和互斥锁 mtx,实现了生产者和消费者之间的同步。​

通过对并行与并发、多线程技术的深入探讨,我们对多线程编程有了更全面的认识。在实际开发中,合理运用这些知识,能够编写出高效、稳定的多线程程序。

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

相关文章:

  • CSP模式下如何保证不抖动
  • 查询去重使用 DISTINCT 的性能分析
  • Ubuntu安装Docker命令清单(以20.04为例)
  • 文件批量重命名
  • Tiktok App 登录账号、密码、验证码 XOR 加密算法
  • C++指针加减法详解:深入理解指针运算的本质
  • ES6 Promise 状态机
  • 外贸建站平台推荐
  • shell脚本的常用命令
  • 2024年认证杯SPSSPRO杯数学建模D题(第二阶段)AI绘画带来的挑战解题全过程文档及程序
  • Linux 命令全讲解:从基础操作到高级运维的实战指南
  • 人脸识别技术应用备案系统已开启!
  • Python趣学篇:Pygame重现《黑客帝国》数字雨
  • ArcGIS Pro 3.4 二次开发 - 地图创作 2
  • 车规级BMS芯片国产化!精准电量监测延长电池寿命
  • JS语法笔记
  • PyTorch——非线性激活(5)
  • Linux系统下Google浏览器无法使用中文输入的临时解决方案
  • AIGC学习笔记(9)——AI大模型开发工程师
  • OD 算法题 B卷【代码编辑器】
  • 第十一章 注解
  • AI数据集构建:从爬虫到标注的全流程指南
  • 使用ArcPy生成地图系列
  • 0518蚂蚁暑期实习上机考试题3:小红的字符串构造
  • 如何爬取google应用商店的应用分类呢?
  • Java-redis实现限时在线秒杀功能
  • 【RAG最新总结】检索增强生成最新进展2024-2025
  • 解决FreePBX 17初始配置时网页无响应
  • CCF CSP 第37次(2025.03)(3_模板展开_C++)(哈希表+stringstream)
  • 【AI学习从零至壹】基于深度学习的⽂本分类任务