0. 前言

在前面我们系统掌握了C++基础语法、面向对象、模板泛型、智能指针、STL全套容器与算法、数据结构与图论核心知识,具备了扎实的单线程程序开发能力

但日常开发、后端服务、高性能程序、服务器开发中,单线程串行执行存在致命短板:同一时间只能执行一段代码、任务串行阻塞、CPU资源无法充分利用、并发处理能力极差。面对海量请求、批量任务、IO阻塞、并行计算场景,单线程程序完全无法满足性能需求。

为此,C++11 标准正式引入std::thread 多线程标准库,彻底告别传统操作系统原生线程的跨平台痛点,实现了跨平台、简洁高效、标准化的多线程编程,是C++高性能并发编程的核心基石。

多线程是C++后端开发、高性能计算、游戏开发、服务器架构的必备核心能力,也是面试高频重难点。很多学习者只会简单创建线程,不懂进程线程本质区别、分不清join与detach、不会正确传递线程参数、不理解线程资源竞争根源,写出来的代码极易出现崩溃、乱序、内存异常、僵尸线程等问题。

今天我们从零吃透C++多线程全套基础体系,从操作系统核心概念、线程创建方式、核心API用法、参数传递避坑、线程分离与等待、到并发资源竞争前置原理,手把手掌握可落地的多线程基础编码规范,为后续锁机制、原子操作、并发编程进阶筑牢根基。

1. 核心前置:进程与线程(面试必背)

1.1 进程定义

进程(Process)是操作系统资源分配的最小单位。每一个独立运行的程序(exe可执行文件),启动后就是一个进程。

进程拥有独立的内存空间、堆内存、文件描述符、系统资源,进程之间完全隔离、互不干扰,切换开销极大。

1.2 线程定义

线程(Thread)是进程内的执行单元,是操作系统CPU调度的最小单位。一个进程默认自带一个主线程,可创建多个子线程。

线程共享进程的堆内存、全局变量、文件资源,仅拥有独立的栈内存,切换开销极小、并发效率极高。

1.3 进程与线程核心区别

对比维度

进程

线程

资源属性

资源分配最小单位

CPU调度最小单位

内存空间

独立内存,进程间完全隔离

共享进程内存,仅栈私有

切换开销

极大,需要切换页表、刷新缓存

极小,仅切换栈与寄存器

通信方式

复杂,需管道、socket、共享内存

简单,直接读写全局/堆变量

生命周期

进程崩溃系统回收资源

子线程崩溃,整个进程直接崩溃

1.4 多线程核心优势

1. 充分利用CPU多核:突破单线程串行限制,并行执行任务,大幅提升程序运行效率;

2. 避免IO阻塞等待:文件读写、网络请求阻塞时,其他线程可正常工作,不卡死主线程;

3. 开销低、并发强:线程创建销毁成本远低于进程,适合高频并发场景;

4. 资源共享便捷:同进程线程共享数据,通信成本极低。

2. C++线程核心基础:std::thread

C++11 及以上标准提供 <thread> 标准库,封装了跨平台线程类,彻底兼容Windows/Linux/macOS,无需适配系统原生API,是现代C++并发编程的基础。

所有线程操作、线程管理、线程等待与分离,全部依托 std::thread 类实现。

2.1 线程创建四大方式

C++支持四种主流线程创建方式,适配不同业务场景:普通函数、仿函数、Lambda表达式、类成员函数。

方式一:普通全局函数创建线程
#include <iostream>
#include <thread>
using namespace std;

// 线程执行函数
void ThreadFunc()
{
    cout << "子线程正在执行" << endl;
}

int main()
{
    // 创建并启动子线程
    thread t1(ThreadFunc);

    // 等待子线程执行完毕
    t1.join();
    cout << "主线程执行结束" << endl;
    return 0;
}
方式二:Lambda表达式创建线程(最常用)

无需单独定义函数,代码简洁紧凑,是日常开发首选方式。

int main()
{
    // Lambda直接作为线程执行体
    thread t2([](){
        cout << "Lambda子线程执行中" << endl;
    });

    t2.join();
    return 0;
}
方式三:仿函数创建线程
class ThreadFunctor
{
public:
    void operator()()
    {
        cout << "仿函数子线程执行中" << endl;
    }
};

int main()
{
    ThreadFunctor func;
    thread t3(func);
    t3.join();
    return 0;
}
方式四:类成员函数创建线程
class Test
{
public:
    void Work()
    {
        cout << "成员函数子线程执行中" << endl;
    }
};

int main()
{
    Test obj;
    // 必须传递对象地址
    thread t4(&Test::Work, &obj);
    t4.join();
    return 0;
}

3. 核心重难点:join() 与 detach()

线程创建后必须进行资源处理,必须二选一执行 join 或 detach,否则程序运行崩溃,这是新手最高频报错点。

3.1 join() 等待线程

核心作用主线程阻塞等待子线程执行完毕,回收子线程资源,主线程再继续向下执行。

适用场景:需要等待子线程任务完成,再执行后续逻辑,保证代码执行顺序可控。

特点:线程资源由主线程统一回收,无资源泄漏,线程生命周期受控。

3.2 detach() 分离线程

核心作用主线程与子线程彻底分离,主线程不再等待子线程,各自独立执行;子线程执行完毕后由操作系统自动回收资源。

适用场景:后台常驻任务、无需关注执行结果、无需等待完成的异步任务。

高危坑点:主线程退出后,进程销毁,所有分离的子线程会被强制终止,可能导致任务未执行完成、数据异常。

3.3 致命禁忌(必记)

1. 禁止对同一线程重复调用 join/detach,直接程序崩溃;

2. 线程对象销毁前,必须完成 join 或 detach,否则析构时报错退出;

3. detach 后的线程无法再被主线程管控,无法获取执行状态。

4. 线程参数传递(高频易错点)

线程传参默认值传递,即使函数形参为引用,也会发生拷贝,这是新手最容易踩的隐形坑。同时直接传临时变量、局部变量极易导致悬空引用问题。

4.1 普通值传递

void Print(int a, int b)
{
    cout << a << " " << b << endl;
}

int main()
{
    int x = 10, y = 20;
    thread t(Print, x, y);
    t.join();
    return 0;
}

4.2 引用传递(必须 std::ref)

想要线程内修改外部变量、实现真正引用传递,必须使用 std::ref() 包裹参数,否则依然是值拷贝。

void Add(int& a)
{
    a += 100;
}

int main()
{
    int num = 10;
    // 必须使用ref实现引用传递
    thread t(Add, ref(num));
    t.join();
    cout << num << endl; // 输出110
    return 0;
}

4.3 指针传递避坑

禁止传递主线程局部临时变量指针!若主线程提前退出销毁局部变量,子线程访问悬空指针,直接内存错乱、程序崩溃。

5. 线程基础属性与状态判断

5.1 joinable() 可连接判断

joinable() 用于判断当前线程是否为有效可连接状态:未执行 join/detach 的线程返回true,已处理或空线程返回false。

工程开发中建议先判断再操作,规避重复操作崩溃问题。

int main()
{
    thread t([](){ cout << "线程执行" << endl; });

    if(t.joinable())
    {
        t.join();
    }
    return 0;
}

5.2 获取线程ID与核心数

// 获取当前线程ID
this_thread::get_id();
// 获取CPU核心数,用于适配并发线程数量
thread::hardware_concurrency();

6. 多线程核心痛点:资源竞争与乱序问题

多线程基础最大的问题,也是并发编程所有bug的根源:多线程共享全局资源,并发读写引发数据错乱

多个线程同时读写同一个全局变量、堆内存数据,CPU交替执行,指令穿插错乱,导致数据覆盖、计数错误、结果异常,这就是线程安全问题

示例:多线程同时累加计数,最终结果小于理论值

#include <iostream>
#include <thread>
using namespace std;

int cnt = 0;

void Add()
{
    for(int i = 0; i < 10000; i++)
    {
        cnt++; // 非线程安全,并发读写冲突
    }
}

int main()
{
    thread t1(Add);
    thread t2(Add);
    t1.join();
    t2.join();
    // 理论20000,实际结果偏小
    cout << cnt << endl;
    return 0;
}

解决该问题的核心方案:互斥锁、原子操作、条件变量,我们将在后续天数专题中深度精讲,今日重点建立线程不安全的核心认知。

7. 基础高频坑点汇总

1.线程未join/detach:线程对象析构前未处理资源,程序直接崩溃;

2. 重复join/detach:对已处理的线程再次操作,触发断言崩溃;

3. 传参默认值拷贝:不使用ref无法实现引用修改,参数修改不生效;

4. detach后主线程提前退出:子线程被强制终止,任务执行不完整;

5. 局部变量指针传参:引发悬空指针、内存访问异常;

6. 多线程裸读写共享变量:无锁操作导致数据错乱、线程不安全。

8. 面试满分问答(必背)

Q1:进程和线程的核心区别?

进程是操作系统资源分配的最小单位,拥有独立内存空间,隔离性强、切换开销大;线程是CPU调度的最小单位,共享进程资源,切换开销小、通信简单,一个进程可包含多个线程。

Q2:join和detach的区别?

join会阻塞主线程,等待子线程执行完毕并手动回收资源,线程生命周期可控;detach分离主线程与子线程,主线程不阻塞,子线程由系统自动回收,生命周期独立,主线程退出会强制终止子线程。

Q3:C++线程传参为什么需要std::ref?

std::thread传参默认是值拷贝,即使形参为引用也无法实现外部变量修改,std::ref可以强制实现真实引用传递,让线程内操作直接作用于外部原变量。

Q4:多线程数据错乱的根本原因?

多个线程并发读写同一共享资源,CPU时间片交替切换,读写指令被穿插、覆盖,导致数据更新丢失、计数异常,引发线程安全问题。

Q5:线程可以重复join吗?

不可以。线程执行一次join或detach后,状态变为非可连接,重复操作会直接触发程序崩溃,开发中需通过joinable()预判线程状态。

9. 全文总结

今天我们彻底吃透了C++多线程全套基础核心。厘清进程与线程的本质区别、掌握std::thread四种线程创建方式、精通join/detach核心机制、搞定线程传参避坑要点、理解线程状态判断与线程不安全根源,建立了完整的多线程基础认知与编码规范。

多线程基础是并发编程的入口,今天所学的所有知识点,是后续互斥锁、死锁、原子操作、线程池、高并发服务开发的前置核心地基,彻底告别“只会创建线程不懂原理”的浅层编程。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐