news 2026/4/21 12:12:11

C++并发编程学习(一)——线程基础

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++并发编程学习(一)——线程基础

文章目录

  • 一、前言
    • 1.1 什么是并发
    • 1.2 为什么使用并发
    • 1.3 并发与C++多线程
  • 二、线程基础
    • 2.1 发起线程
    • 2.2 等待线程完成
    • 2.3 lambda表达式传递
    • 2.4 在后台运行线程
    • 2.5 向线程传递参数

一、前言

1.1 什么是并发

同一个系统中,多个独立活动同时进行,而非依次进行。举个生活中的例子:你一边煮饭一边洗菜,虽然你的手不能同时做两件事,但你可以一会儿切菜、一会儿看锅,整体上两项任务都在推进——这就是并发。

并发的方式有两种:
(1)多进程并发。例如将一个应用软件拆分成多个独立进程同时运行,它们都只含单一线程,非常类似于同时运行浏览器和文字处理软件
(2)多线程并发。线程非常像轻量级进程:每个线程都独立运行,并能各自执行不同的指令序列

注意:并发 ≠ 并行:
并发:多个任务交替执行,可能在单核 CPU 上通过时间片轮转实现。
并行:多个任务真正同时执行,通常需要多核 CPU。

1.2 为什么使用并发

  • 为分离关注点而并发:归类相关代码,隔离无关代码,使程序更易于理解和测试,因此所含缺陷很可能更少。例如一个GUI应用中,主线程:负责响应用户界面事件(点击、拖拽等),必须保持高响应性。工作线程:执行耗时操作(如文件读写、网络请求、复杂计算)
  • 为性能而并发:现代 CPU 通常有多个核心,单线程程序只能利用一个核心,而并发程序可以将任务拆分,在多个核心上真正并行执行,从而缩短总执行时间。再者,很多程序性能瓶颈不在 CPU,而在 I/O 操作(如磁盘读写、网络请求)。这些操作往往需要等待外部设备响应,在此期间 CPU 是空闲的。通过并发,一个线程等待 I/O 时,其他线程可以继续工作,系统整体吞吐量显著提升

1.3 并发与C++多线程

C++98 / C++03:没有线程的年代
🚫 标准中无任何线程概念

  • C++98 和 C++03 标准完全没有定义线程、锁、原子操作等并发原语。
    所有多线程编程必须依赖操作系统 API:
  • Windows:CreateThread, CriticalSection
  • POSIX(Linux/macOS):pthread_create, pthread_mutex_t
  • 代码不可移植,且容易出错(如忘记释放锁、资源泄漏)。

⚠️ 内存模型缺失

  • C++98 没有明确定义内存模型,即多个线程如何观察共享内存的修改顺序。
  • 编译器优化(如指令重排)可能导致多线程程序行为不可预测。
  • 即使使用 volatile,也无法保证原子性或同步。

里程碑:C++11
随着C++11标准的发布,上述种种弊端被一扫而空。C++标准库不仅规定了内存模型,可以区分不同线程,还扩增了新类,分别用于线程管控、保护共享数据、同步线程间操作、以及底层原子操作等。
C++14进一步增添了对并发和并行的支持,具体而言,是引入了一种用于保护共享数据的新互斥。C++17则增添了一系列适合新手的并行算法函数。这两版标准都强化了C++的核心和标准程序库的其他部分,简化了多线程代码的编写

二、线程基础

2.1 发起线程

线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。最简单的任务就是运行一个普通函数,返回空,也不接收参数。函数在自己的线程上运行,等它一返回,线程即随之终止。

#include<thread>voidthead_work1(){std::cout<<"hello thread "<<std::endl;}// 通过()初始化并启动一个线程std::threadt1(thead_work1);

2.2 等待线程完成

当我们启动一个线程后,线程可能没有立即执行,如果在局部作用域启动了一个线程,或者main函数中,很可能子线程没运行就被回收了,回收时会调用线程的析构函数,执行terminate操作。所以为了防止主线程退出或者局部作用域结束导致子线程被析构的情况,我们可以通过join,让主线程等待子线程启动运行,子线程运行结束后主线程再运行。

#include<thread>voidthead_work1(){std::cout<<"hello thread "<<std::endl;}intmain(){std::threadt1(thead_work1);ti.join();// 使用joinreturn0;}

2.3 lambda表达式传递

std::threadt4([](std::string str){std::cout<<"str is "<<str<<std::endl;},hellostr);t4.join();

2.4 在后台运行线程

调用std::thread对象的成员函数detach(),会令线程在后台运行,遂无法与之直接通信。假若线程被分离,就无法等待它完结,也不可能获得与它关联的std::thread对象,因而无法汇合该线程。然而分离的线程确实仍在后台运行,其归属权和控制权都转移给C++运行时库,由此保证,一旦线程退出,与之关联的资源都会被正确回收。

std::threadt(do_background_work);t.detach();assert(!t.joinable());

假设我们有一个程序,希望启动一个后台线程持续写日志,而主线程继续做其他事情,不需要等待日志线程结束:

#include<iostream>#include<thread>#include<chrono>#include<fstream>voidbackground_logger(){std::ofstreamlog("app.log");intcount=0;while(count<10){log<<"Log entry #"<<++count<<"\n";log.flush();// 确保立即写入std::this_thread::sleep_for(std::chrono::seconds(1));}log<<"Logger finished.\n";// 函数返回,线程自然结束}intmain(){std::cout<<"Main: Starting background logger...\n";std::threadlogger_thread(background_logger);// 将线程分离:让它在后台独立运行logger_thread.detach();// 主线程继续工作std::this_thread::sleep_for(std::chrono::milliseconds(3500));std::cout<<"Main: Doing other work...\n";std::this_thread::sleep_for(std::chrono::milliseconds(8000));std::cout<<"Main: Exiting program.\n";// 注意:如果主线程在此退出,而 logger_thread 还没结束,// 程序会终止,后台线程也会被强制杀死!}

detach() 后主线程不等待
主线程打印完 “Exiting program.” 就结束,不会等日志线程写完 10 条日志。

如果主线程结束,整个进程退出,所有线程(都会被操作系统强制终止。
所以上例中 app.log 可能只写了 3~4 行,而不是完整的 10 行

问题说明
访问已销毁的栈变量如果线程函数捕获了主线程的局部变量(尤其是引用或指针),而主线程已退出,会导致悬空指针!
无法处理异常detached 线程中的未捕获异常会导致std::terminate(),且无法被主线程感知。
资源泄漏风险如果线程持有资源(如文件句柄、锁),提前终止可能导致资源未释放。

2.5 向线程传递参数

当线程要调用的回调函数参数为引用类型时,需要将参数显示转化为引用对象传递给线程的构造函数,如果采用如下调用会编译失败

voidproducer_thread(ThreadSafeQueue<int>&tsq){for(inti=0;i<100;i++)tsq.push(i);}voidrun(){ThreadSafeQueue<int>tsq;std::threadt_pro(producer_thread,tsq);}

即使函数change_param的参数为int&类型,我们传递给t2的构造函数为some_param,也不会达到在change_param函数内部修改关联到外部some_param的效果。因为some_param在传递给thread的构造函数后会转变为右值保存,右值传递给一个左值引用会出问题,所以编译出了问题。需要使用std::ref

voidrun(){ThreadSafeQueue<int>tsq;std::threadt_pro(producer_thread,std::ref(tsq));}

有时候传递给线程的参数是独占的,所谓独占就是不支持拷贝赋值和构造,但是我们可以通过 std::move 的方式将参数的所有权转移给线程,如下

voidprocess_big_object(std::unique_ptr<big_object>);std::unique_ptr<big_object>p(newbig_object);p->prepare_data(42);std::threadt(process_big_object,std::move(p));

在调用std::thread的构造函数时,依据std::move§所指定的操作,big_object对象的归属权会发生转移,先进入新创建的线程的内部存储空间,再转移给process_big_object()函数

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 17:59:33

4个维度解析开源工业控制技术与实践

4个维度解析开源工业控制技术与实践 【免费下载链接】OpenPLC Software for the OpenPLC - an open source industrial controller 项目地址: https://gitcode.com/gh_mirrors/op/OpenPLC 开源工业控制技术正逐步打破传统PLC&#xff08;可编程逻辑控制器&#xff09;的…

作者头像 李华
网站建设 2026/4/18 18:43:04

机器人学习数据集构建零门槛指南:从原理到实践的避坑指南

机器人学习数据集构建零门槛指南&#xff1a;从原理到实践的避坑指南 【免费下载链接】lerobot &#x1f917; LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch 项目地址: https://gitcode.com/GitHub_Trending/le/lerobot 机器人学习数…

作者头像 李华
网站建设 2026/4/20 2:24:42

Termux | 基础安装、源站替换与 Linux 环境部署及跨设备远程实操

注&#xff1a;本文为 “Termux” 相关合辑。 图片清晰度受引文原图所限。 略作重排&#xff0c;未整理去重。 如有内容异常&#xff0c;请看原文。 Termux 的安装、换源、基本库安装、基本操作讲解及应用体会 IC 全硅养成记 于 2020-12-15 21:43:27 发布 本文围绕 Android 设…

作者头像 李华
网站建设 2026/4/16 12:44:35

用FSMN-VAD搭建语音预处理系统全过程

用FSMN-VAD搭建语音预处理系统全过程 在语音识别、会议转录、智能客服等AI语音应用落地过程中&#xff0c;一个常被忽视却至关重要的环节是——音频预处理。你是否遇到过这样的问题&#xff1a;一段30分钟的会议录音&#xff0c;真正说话时间只有12分钟&#xff0c;其余全是咳…

作者头像 李华
网站建设 2026/4/18 8:02:26

显存不足如何应对?Z-Image-Turbo_UI界面低配适配法

显存不足如何应对&#xff1f;Z-Image-Turbo_UI界面低配适配法 Z-Image-Turbo 是一款以“快”和“精”见长的开源图像生成模型——8步出图、细节锐利、风格可控。但它的强大背后&#xff0c;对硬件有一定要求&#xff1a;官方推荐显存 ≥12GB&#xff08;如RTX 3090/4080&…

作者头像 李华
网站建设 2026/4/18 14:31:25

DCT-Net人像卡通化API文档:Swagger UI自动生成与测试方法

DCT-Net人像卡通化API文档&#xff1a;Swagger UI自动生成与测试方法 1. 为什么需要API文档&#xff1f;从WebUI到自动化调用的跨越 你已经用过那个点点点就能出卡通头像的网页界面——上传照片、点击按钮、几秒后收获一张萌趣十足的二次元形象。但当你想把这项能力嵌入自己的…

作者头像 李华