《 C++ 零基础入门教程》第8章:多线程与并发编程 —— 让程序“同时做多件事”

✅ 本篇目标:

  • 理解 线程(std::thread 的基本使用
  • 掌握 互斥锁(std::mutex 解决数据竞争
  • 学会使用 std::asyncstd::future 简化异步任务
  • 实战项目:并行计算素数个数 + 线程安全日志系统
  • 初识 死锁避免策略
    🕒 建议学习时间:4--5 小时|可分多次完成

💡 本篇将让你写出 高性能、响应迅速、充分利用多核 CPU 的 C++ 程序!


📘 C++ 零基础入门教程(第 8 篇)

多线程与并发编程 ------ 让程序"同时做多件事"


第一步:为什么需要多线程?

单线程的局限:

cpp 复制代码
void downloadFile() { /* 耗时 5 秒 */ }
void processData() { /* 耗时 3 秒 */ }

int main() {
    downloadFile();  // 等 5 秒
    processData();   // 再等 3 秒 → 总耗时 8 秒
}

✅ 多线程:并行执行

  • 同时下载和处理(如果逻辑允许)
  • 充分利用多核 CPU
  • 提升用户体验(UI 不卡顿)

⚠️ 注意:不是所有任务都能并行!需考虑 数据依赖共享状态


第二步:std::thread ------ 创建线程

基本用法:

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void task1() {
    this_thread::sleep_for(2s); // 模拟耗时操作
    cout << "任务1完成\n";
}

void task2(int id) {
    cout << "任务2(ID=" << id << ")开始\n";
    this_thread::sleep_for(1s);
    cout << "任务2完成\n";
}

int main() {
    thread t1(task1);               // 启动线程 t1
    thread t2(task2, 42);           // 传参启动 t2

    cout << "主线程继续运行...\n";

    t1.join();  // 等待 t1 结束
    t2.join();  // 等待 t2 结束

    cout << "所有任务完成!\n";
    return 0;
}

✅ 可能输出(顺序不确定):

复制代码
主线程继续运行...
任务2(ID=42)开始
任务2完成
任务1完成
所有任务完成!

🔑 关键点:

  • thread t(func, args...) 启动新线程
  • 必须调用 join()detach(),否则程序终止时崩溃
  • join():阻塞当前线程,直到目标线程结束
  • detach():让线程在后台运行(慎用,生命周期难控)

第三步:数据竞争(Data Race)与互斥锁

❌ 危险示例:多个线程修改同一变量

cpp 复制代码
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;  // 非原子操作!可能出错
    }
}

int main() {
    thread t1(increment);
    thread t2(increment);

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

    cout << "最终 counter = " << counter << endl; // 期望 200000,但常小于该值!
}

🔥 数据竞争 :多个线程同时读写同一内存,且无同步 → 未定义行为


✅ 解决方案:std::mutex(互斥锁)

cpp 复制代码
#include <mutex>
int counter = 0;
mutex mtx;  // 互斥锁

void safeIncrement() {
    for (int i = 0; i < 100000; ++i) {
        lock_guard<mutex> lock(mtx); // 自动加锁/解锁(RAII!)
        ++counter;
    }
}

💡 lock_guard 是 RAII 封装:

  • 构造时自动 lock()
  • 析构时自动 unlock()
  • 即使抛异常也能保证解锁!

✅ 现在输出总是:

复制代码
最终 counter = 200000

第四步:std::asyncstd::future ------ 更简单的异步

如果你只是想"启动一个任务,稍后拿结果",asyncthread 更方便。

示例:异步计算平方

cpp 复制代码
#include <future>
#include <iostream>
using namespace std;

int square(int x) {
    this_thread::sleep_for(1s);
    return x * x;
}

int main() {
    // 启动异步任务
    future<int> result = async(square, 10);

    cout << "主线程做其他事...\n";

    // 获取结果(会阻塞直到完成)
    int value = result.get();
    cout << "10 的平方是 " << value << endl;

    return 0;
}

✅ 输出:

复制代码
主线程做其他事...
10 的平方是 100

🎯 优势:

  • 自动管理线程
  • 通过 future 安全传递结果
  • 支持异常传递(见下文)

异常也能跨线程传递!

cpp 复制代码
int mightThrow(int x) {
    if (x < 0) throw runtime_error("负数无效!");
    return x * x;
}

int main() {
    auto f = async(mightThrow, -5);
    try {
        cout << f.get(); // 抛出子线程中的异常!
    } catch (const exception& e) {
        cout << "捕获异常: " << e.what() << endl;
    }
}

✅ 输出:

复制代码
捕获异常: 负数无效!

第五步:实战项目 1 ------ 并行计算 [1, N] 中的素数个数

我们将把大任务拆成小块,并行计算。

cpp 复制代码
// parallel_prime.cpp
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <cmath>
using namespace std;

bool isPrime(int n) {
    if (n < 2) return false;
    if (n == 2) return true;
    if (n % 2 == 0) return false;
    for (int i = 3; i <= sqrt(n); i += 2) {
        if (n % i == 0) return false;
    }
    return true;
}

// 全局结果(需保护)
int totalPrimes = 0;
mutex primeMutex;

void countPrimesInRange(int start, int end) {
    int localCount = 0;
    for (int i = start; i <= end; ++i) {
        if (isPrime(i)) {
            ++localCount;
        }
    }
    // 仅在最后更新全局变量(减少锁开销)
    lock_guard<mutex> lock(primeMutex);
    totalPrimes += localCount;
}

int main() {
    const int N = 100000;
    const int numThreads = 4;
    const int chunkSize = N / numThreads;

    vector<thread> threads;
    
    for (int i = 0; i < numThreads; ++i) {
        int start = i * chunkSize + 1;
        int end = (i == numThreads - 1) ? N : (i + 1) * chunkSize;
        threads.emplace_back(countPrimesInRange, start, end);
    }

    for (auto& t : threads) {
        t.join();
    }

    cout << "1 到 " << N << " 中有 " << totalPrimes << " 个素数\n";
    return 0;
}

💡 优化技巧:

  • 每个线程先计算局部计数,最后一次性加到全局 → 减少锁竞争
  • 对比单线程版本,速度提升接近 4 倍(在 4 核 CPU 上)

第六步:实战项目 2 ------ 线程安全日志系统

多线程程序需要安全地写日志。

cpp 复制代码
// thread_safe_logger.h
#pragma once
#include <iostream>
#include <mutex>
#include <string>

class Logger {
private:
    static std::mutex logMutex;

public:
    template<typename... Args>
    static void log(Args&&... args) {
        std::lock_guard<std::mutex> lock(logMutex);
        ((std::cout << args << " "), ...); // C++17 折叠表达式
        std::cout << std::endl;
    }
};

// 在 .cpp 中定义静态成员
std::mutex Logger::logMutex;

使用:

cpp 复制代码
void worker(int id) {
    for (int i = 0; i < 3; ++i) {
        Logger::log("线程", id, "正在工作,步骤", i);
        this_thread::sleep_for(500ms);
    }
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    t1.join(); t2.join();
    return 0;
}

✅ 输出(无交错乱码):

复制代码
线程 1 正在工作,步骤 0 
线程 2 正在工作,步骤 0 
线程 1 正在工作,步骤 1 
线程 2 正在工作,步骤 1 
...

第七步:死锁(Deadlock)与如何避免

❌ 死锁示例:

cpp 复制代码
mutex m1, m2;

void thread1() {
    lock_guard<mutex> lock1(m1);
    this_thread::sleep_for(1ms);
    lock_guard<mutex> lock2(m2); // 等 m2
}

void thread2() {
    lock_guard<mutex> lock2(m2);
    this_thread::sleep_for(1ms);
    lock_guard<mutex> lock1(m1); // 等 m1 → 死锁!
}

🔥 死锁条件

  1. 互斥
  2. 持有并等待
  3. 不可抢占
  4. 循环等待

✅ 避免策略:

  1. 始终以相同顺序加锁(如先 m1 再 m2)

  2. 使用 std::lock() 一次性锁定多个 mutex:

    cpp 复制代码
    std::lock(m1, m2);
    lock_guard<mutex> lock1(m1, adopt_lock);
    lock_guard<mutex> lock2(m2, adopt_lock);
  3. 尽量减少锁的粒度和持有时间


📌 本篇小结:你已掌握

概念 说明
std::thread 创建和管理线程
数据竞争 多线程同时修改共享数据 → 未定义行为
std::mutex + lock_guard 保证临界区互斥访问
std::async / std::future 简化异步任务与结果获取
线程安全设计 减少共享、最小化锁范围
死锁 成因与避免策略

✅ 下一步建议

  1. 尝试 :用 std::atomic<int> 替代 mutex 实现无锁计数器(适用于简单类型)
  2. 思考 :如何实现"生产者-消费者"模型?(提示:condition_variable
  3. 预习:什么是"移动语义进阶"?右值引用与完美转发

相关推荐
REDcker2 小时前
AIGCJson 库介绍与使用指南
c++·json·aigc·c
ekkcole2 小时前
java实现对excel模版填充保存到本地后合并单元格并通过网络下载
java·开发语言·excel
小郭团队2 小时前
1_1_七段式SVPWM (传统算法反正切)算法理论与 MATLAB 实现详解
人工智能·stm32·嵌入式硬件·算法·dsp开发
setary03012 小时前
c++泛型编程之Typelists
开发语言·c++
u0104058362 小时前
Java应用的链路追踪:实现分布式跟踪
java·开发语言·分布式
翟天保Steven2 小时前
医学影像-CBCT图像重建FDK算法
算法·医学影像·图像重建
这是个栗子2 小时前
【API封装参数传递】params 与 API 封装
开发语言·前端·javascript·data·params
星诺算法备案2 小时前
《算法安全自评估报告》的填报与实操(附模板)
人工智能·算法·备案·算法备案