news 2026/5/1 10:32:13

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/5/1 9:31:02

AI语音也能有情感?VibeVoice情绪表达实测展示

AI语音也能有情感&#xff1f;VibeVoice情绪表达实测展示 你有没有听过这样的AI语音&#xff1a; 读新闻像机器人念字典&#xff0c;讲笑话没一点起伏&#xff0c;安慰人时语气比天气预报还冷淡&#xff1f; 不是AI不会说话&#xff0c;而是大多数TTS系统根本没在“理解”——…

作者头像 李华
网站建设 2026/5/1 10:02:03

ChatGPT各版本效率优化实战:从模型选择到API调优

开篇&#xff1a;一张表格看懂 GPT-3.5 与 GPT-4 的效率差距 先上硬数据&#xff0c;方便你快速判断该用谁。以下结果基于 2024-05 官方公开文档与我在华东阿里云 ECS&#xff08;4 vCPU/8 GB&#xff09;上的实测均值&#xff0c;网络走公网 HTTPS&#xff0c;payload 统一 1…

作者头像 李华
网站建设 2026/5/1 8:54:21

用Z-Image-Turbo做电商主图?科哥定制版落地实操分享

用Z-Image-Turbo做电商主图&#xff1f;科哥定制版落地实操分享 电商运营人最头疼的事之一&#xff0c;就是每天要为几十款商品配图&#xff1a;主图要突出卖点、场景图要营造氛围、细节图要清晰真实——请设计师成本高、外包周期长、自己修图又耗时耗力。直到我试了科哥定制的…

作者头像 李华
网站建设 2026/5/1 8:40:01

Qwen-Image-Edit-2511实战:工业零部件材质替换

Qwen-Image-Edit-2511实战&#xff1a;工业零部件材质替换 在制造业数字化升级过程中&#xff0c;工程师常面临一个高频痛点&#xff1a;同一款机械结构需快速评估多种材质表现——不锈钢的冷峻质感、铝合金的轻盈反光、工程塑料的哑光耐蚀、碳纤维的科技纹理……传统方式依赖…

作者头像 李华
网站建设 2026/4/21 3:09:30

Z-Image-Turbo_UI界面开源吗?代码结构解析

Z-Image-Turbo_UI界面开源吗&#xff1f;代码结构解析 Z-Image-Turbo_UI 是一个轻量、开箱即用的 Web 图形界面&#xff0c;专为 Z-Image-Turbo 文生图模型设计。它不依赖复杂部署&#xff0c;只需一行命令即可启动&#xff0c;通过浏览器访问 http://localhost:7860 即可开始…

作者头像 李华
网站建设 2026/4/30 18:39:30

零门槛浏览器SVG编辑器:SVG-Edit即开即用的矢量图形解决方案

零门槛浏览器SVG编辑器&#xff1a;SVG-Edit即开即用的矢量图形解决方案 【免费下载链接】svgedit Powerful SVG-Editor for your browser 项目地址: https://gitcode.com/gh_mirrors/sv/svgedit 当你需要快速编辑SVG却没有安装专业软件时&#xff0c;当你在不同设备间切…

作者头像 李华