C++多线程简单版(C++11 及以上)

本文第一次接触学习多线程,列举刚学习C++多线程时遇到的概念和基础用法。

编译运行需要设置好C++的版本标准为C++11,因为从C++11标准才开始引入标准化的多线程库。

如果使用C++11之前的版本,C++并不支持原生的多线程开发,需要使用第三方库,比如 Qt 的Qthread、第三方库如Linux的<pthread.h>,Windows的<windows.h> 。

如果不想用C++自带的多线程库,也可以选择第三方库。

文章目录

1. C++多线程核心API

1.1 std::thread 基本使用

std::thread是C++多线程的核心类,用于创建和管理线程。

核心成员函数
函数 作用
join() 等待其他线程执行完毕(主线程阻塞,直到子线程结束)
detach() 分离线程(子线程后台运行,主线程不再管理,也称"守护线程")
joinable() 判断线程是否可被join()detach()(未调用过这两个函数则返回 true )
基础示例:创建简单线程
cpp 复制代码
#include <iostream>
// 必须包含的多线程头文件
#include <thread>

// 线程要执行的函数(普通函数)
void printHello() {
    // 子线程的执行逻辑
    std::cout << "子线程运行中,线程ID:" << std::this_thread::get_id() << std::endl;
}

int main() {
    // 1. 创建线程对象,同时启动子线程(执行printHello函数)
    std::thread t(printHello);

    // 2. 主线程打印信息
    std::cout << "主线程运行中,线程ID:" << std::this_thread::get_id() << std::endl;

    // 3. 等待子线程执行完毕(必须!否则主线程退出会导致子线程被强制终止)
    if (t.joinable()) {
        t.join();
    }

    return 0;
}
代码解释
  • std::thread t(printHello):创建线程对象t,并立即启动子线程,执行printHello函数;
  • std::this_thread::get_id():固定语法。获取当前线程的唯一ID(用于区分不同线程);
  • t.join():主线程暂停,等待子线程执行完printHello后,主线程再继续;
  • 若不调用join(),主线程会直接退出,操作系统会强制终止未完成的子线程,可能导致程序崩溃或资源泄漏。

1.2 线程传参

std::thread支持给线程函数传递参数,注意参数默认按值拷贝 ,若需传递引用,需用std::ref()

示例:线程传参
cpp 复制代码
#include <iostream>
#include <thread>
#include <string>

// 带参数的线程函数
void printMessage(const std::string& msg, int num) {
    std::cout << "子线程接收的参数:" << msg << ",数字:" << num << std::endl;
}

int main() {
    std::string msg = "Hello Thread";
    int num = 100;

    // 传递参数:按值传递(msg和num会拷贝一份给子线程)
    std::thread t(printMessage, msg, num);

    // 等待子线程完成
    if (t.joinable()) {
        t.join();
    }

    // 若需传递引用(避免拷贝),需用std::ref():
    // std::thread t2(printMessage, std::ref(msg), std::ref(num));
    // t2.join();

    return 0;
}

1.3 互斥锁(std::mutex):解决数据竞争

数据竞争:多个线程同时访问同一个共享变量,且至少有一个线程是写操作,会导致数据结果错误(这也是多线程最常见的问题)。

std::mutex(互斥锁)用于保护共享资源:同一时间只有一个线程能获取锁,其他线程需等待,直到锁被释放。

核心成员函数
函数 作用
lock() 获取锁(阻塞,直到拿到锁)
unlock() 释放锁(必须和lock配对)
try_lock() 尝试获取锁(非阻塞,成功返回true,失败返回false)

推荐使用std::lock_guard(RAII封装):自动管理锁的获取和释放,再作用域结束时,会自动解锁,避免忘记unlock()导致死锁。

此外,也可以使用std::unique_lock ,它在 std::lock_guard基础上还可以手动控制锁,更加灵活。

示例:解决多线程累加的竞态条件
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

// 共享变量(多个线程会访问)
int total = 0;
// 互斥锁:保护total
std::mutex mtx;

// 累加函数(子线程执行)
void addNumber(int num) {
    for (int i = 0; i < num; ++i) {
        // 方式1:手动加锁/解锁(不推荐,易忘unlock)
        // mtx.lock();
        // total++;
        // mtx.unlock();

        // 方式2:std::lock_guard(推荐,RAII自动管理)
        std::lock_guard<std::mutex> lock(mtx);
        total++; // 临界区:只有拿到锁的线程能执行
    }
}

int main() {
    // 创建2个线程,各自累加10000次
    std::thread t1(addNumber, 10000);
    std::thread t2(addNumber, 10000);

    // 等待两个线程完成
    t1.join();
    t2.join();

    // 正确结果应该是20000(不加锁会小于20000)
    std::cout << "最终total值:" << total << std::endl;

    return 0;
}
代码解释
  • 若不加互斥锁,total++(本质是load→add→store三步操作)会被两个线程打断,导致最终结果小于20000;
  • std::lock_guard<std::mutex> lock(mtx):创建lock对象时自动调用mtx.lock(),对象销毁时(离开作用域)自动调用mtx.unlock(),即使发生异常也能释放锁;
  • 临界区 :被互斥锁保护的代码段(如上例中的total++),同一时间只能有一个线程执行。

1.4 线程分离(detach)与守护线程

detach()将子线程与主线程分离,子线程后台运行(由操作系统接管),主线程无需等待,也无法再通过join()等待。

示例:分离线程
cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void countDown() {
    for (int i = 5; i > 0; --i) {
        std::cout << "倒计时:" << i << std::endl;
        // 线程休眠1秒(std::chrono是C++11时间库)
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "倒计时结束!" << std::endl;
}

int main() {
    std::thread t(countDown);
    // 分离线程:子线程后台运行,主线程继续执行
    t.detach();

    // 主线程打印信息后休眠3秒(若不休眠,主线程直接退出,子线程可能未执行完)
    std::cout << "主线程继续执行..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "主线程退出!" << std::endl;

    return 0;
}
注意事项
  • 分离后的线程不能再调用join(),否则会崩溃;
  • 主线程退出时,所有分离的子线程会被强制终止(如上例中主线程3秒后退出,子线程的倒计时只到2就被终止);
  • 慎用detach():若子线程访问主线程的局部变量,主线程退出后变量销毁,子线程会访问野指针。

2. 实战案例

2.1 案例1:多线程批量处理任务

需求:创建4个线程,每个线程处理1个任务(打印任务ID和线程ID)。

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

// 任务函数:处理指定ID的任务
void processTask(int taskId) {
    std::cout << "线程ID:" << std::this_thread::get_id() 
              << " 处理任务ID:" << taskId << std::endl;
    // 模拟任务耗时
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::cout << "线程ID:" << std::this_thread::get_id() 
              << " 完成任务ID:" << taskId << std::endl;
}

int main() {
    // 存储线程对象的容器
    std::vector<std::thread> threads;
    const int taskCount = 4;

    // 创建4个线程
    for (int i = 0; i < taskCount; ++i) {
        threads.emplace_back(processTask, i); // emplace_back直接创建线程对象
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        if (t.joinable()) {
            t.join();
        }
    }

    std::cout << "所有任务处理完成!" << std::endl;
    return 0;
}

2.2 案例2:多线程读取文件(简单版)

需求:2个线程分别读取2个文件的内容,并打印到控制台。

cpp 复制代码
#include <iostream>
#include <thread>
#include <fstream>
#include <string>
#include <mutex>

// 互斥锁:保证打印不混乱(多个线程同时打印会导致输出重叠)
std::mutex printMtx;

// 读取文件函数
void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        std::lock_guard<std::mutex> lock(printMtx);
        std::cout << "无法打开文件:" << filename << std::endl;
        return;
    }

    std::string line;
    std::lock_guard<std::mutex> lock(printMtx);
    std::cout << "===== 线程" << std::this_thread::get_id() << " 读取文件:" << filename << " =====" << std::endl;
    
    // 逐行读取文件
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }

    file.close();
}

int main() {
    std::thread t1(readFile, "file1.txt");
    std::thread t2(readFile, "file2.txt");

    t1.join();
    t2.join();

    return 0;
}

3. 多线程编程常见问题与注意事项

3.1 常见问题

  1. 死锁 :多个线程互相等待对方释放锁(如线程A拿锁1等锁2,线程B拿锁2等锁1)。
    • 避免方式:按固定顺序加锁、使用std::lock()同时加多个锁、避免锁嵌套过深。
  2. 数据竞争 :未保护共享变量的读写操作。
    • 避免方式:用互斥锁保护所有共享变量的读写。
  3. 野指针 :子线程访问主线程已销毁的局部变量(尤其detach()的线程)。
    • 避免方式:传递拷贝而非引用、确保子线程生命周期短于共享变量。
  4. 过度线程化 :创建过多线程(超过CPU核心数),导致线程切换开销大于并行收益。
    • 避免方式:根据CPU核心数设置线程数(可通过std::thread::hardware_concurrency()获取核心数)。

3.2 入门级注意事项

  1. 编译时必须加-pthread(g++/clang)或链接线程库(MSVC);
  2. std::thread对象创建后必须调用join()detach(),否则析构时会崩溃;
  3. 优先使用std::lock_guard/std::unique_lock(进阶)而非手动lock()/unlock()
  4. 线程函数的参数若为临时变量,需确保生命周期覆盖线程执行时间。

4. 总结与进阶

4.1 总结

  1. C++11 提供<thread>库实现跨平台多线程,核心类是std::thread
  2. 多线程的核心问题是数据竞争 ,需用std::mutex(配合std::lock_guard)保护共享资源;
  3. join()等待线程完成,detach()分离线程(慎用);
  4. 多线程编程的关键是"保护共享资源,避免竞态条件"。

4.2 进阶方向

  1. 条件变量(std::condition_variable):实现线程间的同步(如生产者-消费者模型);
  2. 原子操作(std::atomic):轻量级的共享变量保护(无需锁,性能更高);
  3. 线程池:复用线程,避免频繁创建/销毁线程的开销;
  4. 异步编程(std::async/std::future):更简洁的异步任务处理;
  5. C++17/C++20 新特性 :如std::jthread(自动join的线程)、协程等。

附:编译运行命令

Linux/macOS(g++/clang)
bash 复制代码
# 编译(指定C++11标准+链接线程库)
g++ -std=c++11 -pthread thread_demo.cpp -o thread_demo
# 运行
./thread_demo
Windows(MSVC)
powershell 复制代码
# 编译(VS开发者命令提示符)
cl /std:c++11 /EHsc thread_demo.cpp
# 运行
thread_demo.exe

持续更新中,,,,,

本次只是初步认识,第二次学习将补充完善,原子量、条件变量、wait 等内容,并比较不同用法区别。

相关推荐
君义_noip1 小时前
信息学奥赛一本通 2134:【25CSPS提高组】道路修复 | 洛谷 P14362 [CSP-S 2025] 道路修复
c++·算法·图论·信息学奥赛·csp-s
liulilittle2 小时前
OPENPPP2 Code Analysis One
网络·c++·网络协议·信息与通信·通信
Morwit2 小时前
*【力扣hot100】 647. 回文子串
c++·算法·leetcode
天赐学c语言2 小时前
1.7 - 删除排序链表中的重要元素II && 哈希冲突常用解决冲突方法
数据结构·c++·链表·哈希算法·leecode
w陆压2 小时前
12.STL容器基础
c++·c++基础知识
龚礼鹏3 小时前
Android应用程序 c/c++ 崩溃排查流程二——AddressSanitizer工具使用
android·c语言·c++
qq_401700414 小时前
QT C++ 好看的连击动画组件
开发语言·c++·qt
额呃呃4 小时前
STL内存分配器
开发语言·c++
七点半7704 小时前
c++基本内容
开发语言·c++·算法
嵌入式进阶行者4 小时前
【算法】基于滑动窗口的区间问题求解算法与实例:华为OD机考双机位A卷 - 最长的顺子
开发语言·c++·算法