C++11 引入了标准线程库,使得多线程编程变得更加简单和可移植。下面是一个快速入门指南,帮助你理解C++多线程编程的基本概念。

基本概念:
        线程:程序中的执行流

        并发:多个任务交替执行(单核CPU)

        并行:多个任务同时执行(多核CPU)

创建线程(thread)

想象一下你在厨房做饭(主线程),同时让助手去超市买调料(新线程):

        创建线程:你对助手说"去超市买酱油"

        join():你站在厨房门口等待,直到助手回来才继续做饭

不join():你不等助手回来,继续做其他事情,但可能会遇到问题 - 比如需要酱油时助手还没回来

#include <iostream>
#include <thread>
#include <chrono>

// 助手线程的任务:模拟去超市买东西
void goToSupermarket(int timeNeeded) {
    std::cout << "助手: 开始去超市,需要 " << timeNeeded << " 秒" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(timeNeeded));
    std::cout << "助手: 买完东西回来了!" << std::endl;
}

int main() {
    std::cout << "主厨: 开始准备晚餐" << std::endl;
    
    // 创建助手线程(让助手去买东西)
    std::thread assistant(goToSupermarket, 3);
    
    std::cout << "主厨: 助手已出发,我可以先做其他准备工作..." << std::endl;
    
    // join() 的含义:主厨等待助手回来
    std::cout << "主厨: 等待助手回来..." << std::endl;
    assistant.join();  // 这里主线程(主厨)会阻塞,直到助手线程完成
    
    // 只有在助手回来后,才会执行下面的代码
    std::cout << "主厨: 助手回来了,现在可以用买来的调料继续做菜!" << std::endl;
    std::cout << "主厨: 晚餐完成!" << std::endl;
    
    return 0;
}

'''
输出结果:
主厨: 开始准备晚餐
助手: 开始去超市,需要 3 秒
主厨: 助手已出发,我可以先做其他准备工作...
主厨: 等待助手回来...
助手: 买完东西回来了!
主厨: 助手回来了,现在可以用买来的调料继续做菜!
主厨: 晚餐完成!
'''

基本用法总结:

        std::thread 构造函数接受一个可调用对象和其参数,线程在构造后立即开始执行。

        join() 方法阻塞当前线程,直到目标线程执行完成

互斥量(mutex)

        互斥量 是“互斥锁”的简称,用于保护共享数据,防止多个线程同时访问。因为在多线程环境中,如果多个线程不加控制地同时读写同一块数据,会导致结果不可预测。

        可以把它想象成一个卫生间的门,门上有一把钥匙(互斥量)。这个卫生间(共享资源)一次只能有一个人(线程)使用。流程是这样的:

        一个人来了,拿到钥匙(lock),进入卫生间,并锁上门。

        其他想来用卫生间的人发现钥匙没了(被lock了),就必须等待(被阻塞)。

        里面的人用完了,出来并把钥匙放回原处(unlock)。

        等待的人中有一个可以拿到钥匙,然后进入。

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx; // 定义一个全局的互斥量

void safe_increment() {
    for (int i = 0; i < 1000000; ++i) {
        mtx.lock();   // 上锁:进入临界区
        counter++;    // 现在这个操作是安全的了
        mtx.unlock(); // 解锁:离开临界区
    }
}

int main() {
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

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

    std::cout << "Counter value: " << counter << std::endl; // 结果必定是 2000000
    return 0;
}

        但是,手动调用 lock() 和 unlock() 非常危险!如果在临界区中发生异常或提前返回,unlock() 可能不会被调用,导致死锁(其他线程永远在等待)。

        C++ 遵循 RAII 原则,提供了几个锁管理器类,它们在构造时自动上锁,在析构时自动解锁,确保异常安全。

        RAII 是 Resource Acquisition Is Initialization 的缩写,中文直译为“资源获取即初始化”。RAII 用 C++ 对象析构函数一定会被执行的特性,来确保资源一定会被安全地释放。利

'''
1. std::lock_guard (C++11)
最简单的锁管理器,在构造时上锁,析构时解锁。不支持手动解锁。
'''
void safe_increment() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 构造函数中调用 mtx.lock()
        counter++;
        // lock 析构时自动调用 mtx.unlock()
    }
}



'''
2. std::unique_lock (C++11)
更灵活的锁管理器。除了 std::lock_guard 的功能外,还支持:
    延迟上锁:构造时不立即上锁。
    手动解锁和重新上锁。
    移动语义。
'''
void safe_increment() {
    for (int i = 0; i < 1000000; ++i) {
        std::unique_lock<std::mutex> ulock(mtx, std::defer_lock); // 延迟上锁
        // ... 这里可以执行一些不需要锁的操作 ...
        ulock.lock(); // 现在才上锁
        counter++;
        ulock.unlock(); // 可以手动提前解锁
        // ... 这里可以执行一些不需要锁的操作 ...
        // ulock 析构时,如果还持有锁,会自动解锁
    }
}

条件变量 (condition variable)

条件变量,允许一个或多个线程等待,直到另一个线程修改了共享数据(即某个“条件”满足)并通知条件变量。

简单来说,它就是线程间进行通信的一种机制,用于一个线程等待特定事件发生,而另一个线程在事件发生后通知等待的线程。

想象一个经典的生产者-消费者场景:

        生产者线程:向一个队列(共享数据)中放入数据。

        消费者线程:从队列中取出数据。

        如果没有条件变量,消费者线程可能需要不断地循环检查队列是否为空(“忙等待”或“自旋”),这非常浪费 CPU 资源。

        条件变量解决了这个问题:它让消费者线程在队列为空时进入“等待”状态,主动释放 CPU。当生产者向队列放入数据后,再“通知”消费者,消费者被唤醒并从等待处继续执行。这大大提高了效率。

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

std::queue<int> g_queue; // 共享队列
std::mutex g_mutex; // 保护队列的互斥锁
std::condition_variable g_cv; // 条件变量

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            // 操作共享数据前加锁
            std::unique_lock<std::mutex> lock(g_mutex);
            g_queue.push(i);
            std::cout << "Produced: " << i << std::endl;
        } // 这里 lock 超出作用域,自动释放锁

        // 通知一个等待的消费者
        g_cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(g_mutex);

        // 等待条件满足(队列非空)
        // 使用带谓词的 wait,防止虚假唤醒
        // 谓词 (Predicate) 是什么?
        //在 condition_variable::wait 的语境下,谓词是一个可调用对象
        //(比如函数、lambda 表达式、函数对象),它返回 true 或 false。
        //它的唯一职责就是检查我们真正等待的“条件”是否已经满足。 它通常
        //是一个简单的检查语句,访问被互斥锁保护的共享数据,并返回一个布
        //尔值。核心思想: 我们等待的并不是条件变量本身,而是由谓词定义的
        //某个特定条件变为真。条件变量只是实现这个等待过程的机制。
        // lambda 函数 `[&](){ return !g_queue.empty(); }` 就是谓词 (Predicate)
        g_cv.wait(lock, [&]() { 
            return !g_queue.empty(); 
        });

        // 走到这里,锁已经被重新获取,并且队列肯定不为空
        int data = g_queue.front();
        g_queue.pop();
        std::cout << "Consumed: " << data << std::endl;

        // 如果收到结束信号(例如数据为-1),可以退出循环
        // if (data == -1) break;
    }
}

int main() {
    std::thread prod_thread(producer);
    std::thread cons_thread(consumer);

    prod_thread.join();
    cons_thread.join();

    return 0;
}

原子操作(atomic operation)

原子操作提供无锁的线程安全操作,适用于简单的计数器等场景。

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

std::atomic<int> atomicCounter(0);
int nonAtomicCounter = 0;

void incrementAtomic() {
    for (int i = 0; i < 100000; ++i) {
        ++atomicCounter;
    }
}

void incrementNonAtomic() {
    for (int i = 0; i < 100000; ++i) {
        ++nonAtomicCounter;
    }
}

int main() {
    // 测试原子操作
    std::vector<std::thread> atomicThreads;
    for (int i = 0; i < 10; ++i) {
        atomicThreads.emplace_back(incrementAtomic);
    }
    
    for (auto& t : atomicThreads) {
        t.join();
    }
    
    // 测试非原子操作(结果不确定)
    std::vector<std::thread> nonAtomicThreads;
    for (int i = 0; i < 10; ++i) {
        nonAtomicThreads.emplace_back(incrementNonAtomic);
    }
    
    for (auto& t : nonAtomicThreads) {
        t.join();
    }
    
    std::cout << "Atomic counter: " << atomicCounter << std::endl;
    std::cout << "Non-atomic counter: " << nonAtomicCounter << std::endl;
    
    return 0;
}

异步操作(asynchronous operation

        异步操作,允许你启动一个任务后,无需等待其完成,就可以继续执行后续的代码。当需要该任务的结果时,再去获取它。这是一种提高程序性能和响应能力的关键技术,特别是在需要处理耗时操作(如I/O读写、网络请求、复杂计算)时。

使用异步操作:

        你告诉系统:“请在未来某个时候帮我执行 CalculateHugeData() 这个任务”。

        系统说:“好的,这是你的‘凭证’(future),你先去忙别的吧”。

        你的主程序可以继续流畅运行,响应用户操作、更新UI等。

        当你真正需要那个计算结果时,你拿出之前的“凭证”问:“任务完成了吗?把结果给我”。如果完成了就直接拿到结果;如果还没完成,你可以选择等待或稍后再试。

#include <iostream>
#include <future> // 核心头文件
#include <chrono>
#include <thread>

// 一个耗时的函数
int CalculateSomething(int x, int y) {
    // 假设这个计算很耗时
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x + y;
}

int main() {
    // 使用 std::async 异步地启动 CalculateSomething 任务
    // std::launch::async 策略表示一定要在新线程中执行
    std::future<int> result_future = std::async(std::launch::async, CalculateSomething, 10, 20);

    // 主线程(当前线程)不会阻塞,可以继续做其他工作
    std::cout << "主线程正在处理其他任务..." << std::endl;
    // 模拟主线程也做一些工作
    std::this_thread::sleep_for(std::chrono::seconds(1)); 
    std::cout << "主线程其他工作做完了,现在需要异步计算的结果了。" << std::endl;

    // 使用 get() 获取异步操作的结果。
    // 如果异步计算还没完成,get() 会阻塞当前线程,直到结果就绪。
    int result = result_future.get();

    std::cout << "计算结果是: " << result << std::endl; // 输出 30

    return 0;
}

'''
结果输出:
主线程正在处理其他任务...
(等待约1秒...)
主线程其他工作做完了,现在需要异步计算的结果了。
(再等待约1秒,因为异步任务总共需要2秒,此时已过去1秒,还差1秒)
计算结果是: 30
'''

更多推荐