Skip to content

C++多线程编程:从入门到精通,构建高性能并发应用,非常详细,强烈建议收藏!!!

在当今多核处理器普及的时代,单线程程序已无法充分榨取硬件性能。多线程编程应运而生,它赋予程序同时处理多个任务的能力,从而显著提升应用的响应速度、吞吐量和资源利用率。然而,并发的强大力量也伴随着复杂性:数据竞争、死锁、活锁、饥饿等问题层出不穷,对开发者提出了更高的要求。

C++11及后续标准(如C++14, C++17, C++20)为多线程编程提供了强大而完善的标准库支持,使得C++开发者能够更安全、高效地编写并发程序。本文旨在为您提供一份全面、深入且实用的C++多线程编程指南,助您从原理到实践,驾驭并发编程的艺术。

一、多线程基础:std::thread的初探

1. 进程与线程:并发的基石

在深入多线程之前,理解进程与线程的本质区别至关重要:

  • 进程(Process):操作系统分配资源(如内存、文件句柄)的基本单位。每个进程拥有独立的内存空间,进程间通信(IPC)相对复杂。
  • 线程(Thread):CPU调度执行的基本单位,是进程内部的一个执行流。同一进程内的所有线程共享进程的内存空间和资源,但每个线程拥有独立的栈、程序计数器和寄存器。线程间通信更高效,但需谨慎处理共享数据。

核心区别速览

特性进程线程
资源独立,开销大共享进程资源,开销小
调度操作系统调度CPU调度
通信IPC机制共享内存,更高效
健壮性独立性强,一个崩溃不影响其他一个崩溃可能影响整个进程

2. 为什么选择多线程?

  • 提高响应速度:对于GUI应用或网络服务,多线程可以避免主线程阻塞,保持用户界面的响应性或服务的可用性。
  • 提高吞吐量:在多核处理器上,多个线程可以并行执行,从而提高单位时间内完成的任务数量。
  • 提高资源利用率:当一个任务需要等待I/O操作时,其他线程可以继续执行CPU密集型任务,充分利用CPU资源。
  • 简化程序设计:将复杂任务分解为多个独立子任务,每个子任务由一个线程处理,使程序结构更清晰。

3. std::thread:C++11的线程利器

std::thread是C++标准库中用于创建和管理线程的核心类。它封装了底层线程操作,提供了简洁的API。

基本用法:

std::thread的构造函数接受一个可调用对象(函数、函数对象、Lambda表达式)作为线程的入口点,以及可选的参数。线程在std::thread对象创建时即开始执行。

cpp
#include <iostream>
#include <thread>
#include <chrono> // 用于std::this_thread::sleep_for
#include <string>
#include <vector>

// 1. 使用普通函数作为线程入口
void simple_thread_func() {
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Hello from simple_thread_func!\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

// 2. 使用Lambda表达式作为线程入口,并捕获外部变量
void lambda_thread_example() {
    int shared_val = 0;
    // 注意:Lambda捕获变量时,如果以值捕获,则在新线程中是拷贝;如果以引用捕获,则需注意生命周期问题。
    std::thread t_lambda([&shared_val]() {
        for (int i = 0; i < 3; ++i) {
            shared_val++; // 修改外部变量
            std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Lambda thread: shared_val = " << shared_val << "\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }
    });
    t_lambda.join(); // 等待lambda线程完成
    std::cout << "[Main Thread] Lambda thread finished, final shared_val: " << shared_val << "\n";
}

// 3. 线程传参:注意拷贝与引用
void thread_with_args(int id, std::string& msg) {
    // msg 是引用传递,会修改原始字符串
    msg += " (modified by thread)";
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Thread " << id << ": " << msg << "\n";
}

int main_thread_basic_usage() {
    std::cout << "[Main Thread] Main thread starts.\n";

    // 创建并启动线程
    std::thread t1(simple_thread_func);

    // 传参示例:std::ref 用于引用传递,否则默认是值拷贝
    std::string original_msg = "Hello from main!";
    std::thread t2(thread_with_args, 42, std::ref(original_msg));

    // 主线程继续执行其他任务
    std::cout << "[Main Thread] Main thread doing other work...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(150));

    // 等待子线程完成:join()会阻塞当前线程直到子线程结束
    t1.join();
    t2.join();

    std::cout << "[Main Thread] Original message after t2: " << original_msg << "\n";
    std::cout << "[Main Thread] Main thread ends.\n\n";

    lambda_thread_example(); // 演示Lambda线程示例

    return 0;
}

线程生命周期管理:join()detach()

std::thread对象在析构时,如果其关联的线程仍然可joinable(即线程仍在运行且未被join或detach),程序会调用std::terminate终止。因此,必须显式管理线程的生命周期:

  • join():阻塞当前线程,直到std::thread对象关联的线程执行完毕。join()后,线程资源被回收,std::thread对象不再关联任何线程。
  • detach():将std::thread对象与它关联的线程分离。分离后,线程作为后台线程独立运行,其生命周期不再受std::thread对象控制。当后台线程执行完毕时,其资源由C++运行时库自动回收。detach()后,std::thread对象也变为不可joinable。

重要提示:std::thread对象是可移动(Movable)但不可拷贝(Non-copyable)的。

这意味着你可以通过std::move转移线程所有权,但不能直接拷贝。

cpp
#include <iostream>
#include <thread>
#include <vector>
#include <utility> // For std::move

void worker_task() {
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Worker task running.\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

int main_thread_lifecycle() {
    std::cout << "[Main Thread] Starting thread lifecycle example.\n";

    std::thread t_initial(worker_task);

    // std::thread t_copy = t_initial; // 编译错误:std::thread 不可拷贝

    std::thread t_moved = std::move(t_initial); // 所有权转移
    // 此时 t_initial 不再关联任何线程,t_moved 关联了原来的线程

    if (!t_initial.joinable()) {
        std::cout << "[Main Thread] t_initial is no longer joinable (ownership moved).\n";
    }

    // 使用 vector 管理多个线程
    std::vector<std::thread> threads_vec;
    for (int i = 0; i < 3; ++i) {
        // emplace_back 直接在容器内构造线程对象,避免拷贝
        threads_vec.emplace_back(worker_task);
    }

    // 等待所有线程完成
    if (t_moved.joinable()) {
        t_moved.join();
    }
    for (std::thread& t : threads_vec) {
        if (t.joinable()) {
            t.join();
        }
    }

    std::cout << "[Main Thread] All threads joined. Example finished.\n";
    return 0;
}

二、线程同步机制:共享数据的守护者

多线程共享数据是并发编程的常见场景,也是数据竞争(Data Race)的温床。当多个线程同时读写同一块内存区域且至少有一个是写操作时,若不加控制,程序行为将不可预测。线程同步机制正是为了解决这一问题,确保共享数据的正确性和一致性。

1. 互斥量(Mutex):最基本的锁

互斥量(Mutual Exclusion)是C++中最基础的同步原语,用于保护共享资源,确保在任何时刻只有一个线程可以访问被保护的代码段(临界区)。

原理:

互斥量像一把独占的锁。线程在访问临界区前必须先“锁定”互斥量。如果互斥量已被其他线程锁定,当前线程将被阻塞,直到锁被释放。访问完成后,线程必须“解锁”互斥量。

std::mutex:C++11提供的标准互斥量。

cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx_counter; // 全局互斥量
long long global_counter = 0;

void increment_counter_mutex() {
    for (int i = 0; i < 100000; ++i) {
        mtx_counter.lock(); // 锁定互斥量
        global_counter++;   // 访问临界区
        mtx_counter.unlock(); // 解锁互斥量
    }
}

int main_mutex_basic() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter_mutex);
    }

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

    std::cout << "Final global_counter (mutex): " << global_counter << std::endl; // 预期输出 1000000
    return 0;
}

RAII风格的锁管理:std::lock_guardstd::unique_lock

直接使用lock()unlock()容易因忘记解锁或异常导致死锁。C++推荐使用RAII(Resource Acquisition Is Initialization)机制的锁管理类,确保锁的自动释放。

  • std::lock_guard<std::mutex>
    • 特点:构造时自动锁定,析构时自动解锁。简单、高效,适用于简单的临界区保护。
    • 限制:不可移动、不可拷贝,不支持手动解锁。
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx_guard_example;
long long global_counter_guard = 0;

void increment_counter_guard() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx_guard_example); // 构造时锁定,离开作用域时自动解锁
        global_counter_guard++;
    }
}

int main_lock_guard() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter_guard);
    }
    for (std::thread& t : threads) {
        t.join();
    }
    std::cout << "Final global_counter (lock_guard): " << global_counter_guard << std::endl;
    return 0;
}
  • std::unique_lock<std::mutex>
    • 特点:比std::lock_guard更灵活。支持手动锁定/解锁、延迟锁定(std::defer_lock)、尝试锁定(try_lock)、所有权转移等。
    • 开销:略高于std::lock_guard
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx_unique_example;
long long global_counter_unique = 0;

void increment_counter_unique() {
    for (int i = 0; i < 100000; ++i) {
        std::unique_lock<std::mutex> lock(mtx_unique_example); // 构造时锁定
        global_counter_unique++;
        // lock.unlock(); // 可以手动解锁
        // lock.lock();   // 也可以再次锁定
    }
}

int main_unique_lock() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter_unique);
    }
    for (std::thread& t : threads) {
        t.join();
    }
    std::cout << "Final global_counter (unique_lock): " << global_counter_unique << std::endl;
    return 0;
}

死锁及其避免:

死锁是并发编程中的经典问题:两个或多个线程在竞争资源时,每个线程都持有部分资源并等待获取对方持有的资源,导致所有线程都无法继续执行。

避免死锁的策略:

  1. 固定加锁顺序:所有线程都按照相同的顺序获取多个互斥量。
  2. 使用std::lock()std::lock(mtx1, mtx2, ...)可以原子性地锁定多个互斥量,并自动处理死锁。它会尝试锁定所有互斥量,如果不能全部锁定,则会释放已获得的锁,然后重新尝试。
  3. 避免嵌套锁:尽量避免在一个已持有锁的临界区内再次尝试获取另一个锁。
  4. 使用std::unique_locktry_lockdefer_lock:尝试非阻塞地获取锁,如果失败则可以执行其他操作或等待。
cpp
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx_A;
std::mutex mtx_B;

// 潜在死锁的函数:线程1先锁A再锁B,线程2先锁B再锁A
void thread_func_deadlock_A_B() {
    std::lock_guard<std::mutex> lock_a(mtx_A);
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 制造竞争条件
    std::lock_guard<std::mutex> lock_b(mtx_B);
    std::cout << "Thread A-B: Acquired both locks.\n";
}

void thread_func_deadlock_B_A() {
    std::lock_guard<std::mutex> lock_b(mtx_B);
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 制造竞争条件
    std::lock_guard<std::mutex> lock_a(mtx_A);
    std::cout << "Thread B-A: Acquired both locks.\n";
}

// 使用 std::lock 避免死锁
void thread_func_safe_lock_1() {
    // std::lock 会尝试同时锁定所有互斥量,避免死锁
    std::lock(mtx_A, mtx_B);
    // 使用 std::adopt_lock 告知 lock_guard 互斥量已被锁定
    std::lock_guard<std::mutex> lock_a(mtx_A, std::adopt_lock);
    std::lock_guard<std::mutex> lock_b(mtx_B, std::adopt_lock);
    std::cout << "Thread Safe 1: Acquired both locks.\n";
}

void thread_func_safe_lock_2() {
    std::lock(mtx_B, mtx_A);
    std::lock_guard<std::mutex> lock_b(mtx_B, std::adopt_lock);
    std::lock_guard<std::mutex> lock_a(mtx_A, std::adopt_lock);
    std::cout << "Thread Safe 2: Acquired both locks.\n";
}

int main_deadlock_and_safe_lock() {
    std::cout << "--- Potential Deadlock Example ---\n";
    std::thread t_dl1(thread_func_deadlock_A_B);
    std::thread t_dl2(thread_func_deadlock_B_A);
    t_dl1.join();
    t_dl2.join();
    std::cout << "\n--- Safe Lock Example with std::lock ---\n";
    std::thread t_safe1(thread_func_safe_lock_1);
    std::thread t_safe2(thread_func_safe_lock_2);
    t_safe1.join();
    t_safe2.join();
    return 0;
}

2. 条件变量(Condition Variable):线程间的协作

条件变量(std::condition_variable)通常与互斥量一起使用,用于实现线程间的等待和通知机制。一个线程可以等待某个条件成立,而另一个线程可以在条件成立时通知等待的线程。

原理:

条件变量允许线程在满足特定条件前挂起,并在条件满足时被唤醒。它通过wait()方法释放互斥量并阻塞,通过notify_one()notify_all()唤醒等待线程。

std::condition_variable

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

std::mutex mtx_cv_example;
std::condition_variable cv_example;
std::queue<int> data_buffer; // 共享数据缓冲区
bool producer_done = false;

// 生产者线程
void producer_cv() {
    for (int i = 0; i < 5; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::unique_lock<std::mutex> lock(mtx_cv_example);
        data_buffer.push(i); // 生产数据
        std::cout << "[Producer] Produced: " << i << std::endl;
        cv_example.notify_one(); // 通知一个等待的消费者
    }
    std::unique_lock<std::mutex> lock(mtx_cv_example);
    producer_done = true;
    cv_example.notify_all(); // 生产结束,通知所有消费者
}

// 消费者线程
void consumer_cv(int id) {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx_cv_example);
        // wait()的第二个参数是谓词(predicate),用于检查条件是否满足
        // 虚假唤醒(Spurious Wakeup)可能发生,因此必须在循环中检查条件
        cv_example.wait(lock, [&]{
            return !data_buffer.empty() || producer_done; 
        });

        if (data_buffer.empty() && producer_done) {
            std::cout << "[Consumer " << id << "] No more data, exiting.\n";
            break; 
        }

        int data = data_buffer.front();
        data_buffer.pop();
        std::cout << "[Consumer " << id << "] Consumed: " << data << std::endl;
        // lock.unlock(); // 可以在处理数据时解锁,提高并发度
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main_condition_variable() {
    std::thread prod_t(producer_cv);
    std::thread cons_t1(consumer_cv, 1);
    std::thread cons_t2(consumer_cv, 2);

    prod_t.join();
    cons_t1.join();
    cons_t2.join();

    std::cout << "[Main Thread] Condition variable example finished.\n";
    return 0;
}

3. 递归互斥量(std::recursive_mutex):可重入的锁

std::recursive_mutex允许同一个线程多次锁定它而不会造成死锁。每次锁定都需要对应一次解锁。它适用于一个函数内部需要多次加锁,或者一个加锁的函数调用了另一个也需要加锁的函数的情况。

原理:

std::recursive_mutex内部维护一个计数器和拥有者线程ID。当线程首次锁定它时,计数器置1并记录线程ID;同一线程再次锁定,计数器递增。只有当计数器减为0时,锁才真正释放。

使用场景:

  • 可重入函数:当一个函数可能在持有锁的情况下,再次调用自身或调用另一个需要相同锁的函数时。

注意:虽然方便,但滥用可能掩盖设计问题。通常应优先考虑重构代码以避免递归加锁。

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

std::recursive_mutex rec_mtx_example;

void recursive_lock_func(int count) {
    if (count <= 0) {
        return;
    }
    std::lock_guard<std::recursive_mutex> lock(rec_mtx_example);
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Locked, count: " << count << std::endl;
    recursive_lock_func(count - 1);
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Unlocked, count: " << count << std::endl;
}

int main_recursive_mutex() {
    std::thread t1(recursive_lock_func, 3);
    std::thread t2(recursive_lock_func, 2);

    t1.join();
    t2.join();
    return 0;
}

4. 定时互斥量(std::timed_mutex):带超时的锁

std::timed_mutexstd::mutex的扩展,提供了尝试在指定时间内锁定互斥量的功能,避免了无限期等待。

原理:

它提供了try_lock_for()try_lock_until()方法。这些方法会在尝试获取锁失败时,等待一段指定的时间,如果在这段时间内成功获取锁则返回true,否则返回false

使用场景:

  • 避免无限期阻塞:当线程不能无限期等待锁时,可以尝试在一定时间内获取锁,如果超时则执行其他逻辑。
  • 资源竞争不激烈:在资源竞争不激烈但又需要避免长时间阻塞的场景。
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex timed_mtx_example;

void task_with_timeout_lock() {
    std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Trying to acquire lock...\n";
    // 尝试在1秒内获取锁
    if (timed_mtx_example.try_lock_for(std::chrono::seconds(1))) {
        std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Acquired lock.\n";
        std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟持有锁的时间
        timed_mtx_example.unlock();
        std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Released lock.\n";
    } else {
        std::cout << "[Thread ID: " << std::this_thread::get_id() << "] Could not acquire lock within 1 second.\n";
    }
}

int main_timed_mutex() {
    std::thread t1(task_with_timeout_lock);
    std::thread t2(task_with_timeout_lock);

    t1.join();
    t2.join();
    return 0;
}

5. 读写互斥量(std::shared_mutex):读写分离锁

std::shared_mutex(C++17引入)允许多个线程同时读取共享资源(共享锁),但只允许一个线程写入共享资源(独占锁)。这在读多写少的场景下能显著提高并发性能。

原理:

std::shared_mutex维护两种锁:

  • 共享锁(Shared Lock):多个线程可以同时持有,用于读取操作。
  • 独占锁(Exclusive Lock):只有一个线程可以持有独占锁,用于写入操作。当有线程持有独占锁时,其他任何线程都不能获取共享锁或独占锁;当有线程持有共享锁时,其他线程可以继续获取共享锁,但不能获取独占锁。

std::shared_lockstd::unique_lock配合std::shared_mutex

  • std::shared_lock<std::shared_mutex>:用于获取共享锁(读锁)。
  • std::unique_lock<std::shared_mutex>:用于获取独占锁(写锁)。

使用场景:

  • 读多写少的数据结构:例如缓存、配置信息等,大部分操作是读取,少量操作是修改。
cpp
#include <iostream>
#include <thread>
#include <shared_mutex> // C++17
#include <vector>
#include <string>

std::shared_mutex sh_mtx_example;
std::string shared_data_rw = "Initial Data";

void read_data_sh(int id) {
    for (int i = 0; i < 3; ++i) {
        std::shared_lock<std::shared_mutex> lock(sh_mtx_example); // 获取共享锁 (读锁)
        std::cout << "[Reader " << id << "] Reading: " << shared_data_rw << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void write_data_sh(int id, const std::string& new_data) {
    std::unique_lock<std::shared_mutex> lock(sh_mtx_example); // 获取独占锁 (写锁)
    std::cout << "[Writer " << id << "] Writing...\n";
    shared_data_rw = new_data;
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    std::cout << "[Writer " << id << "] Finished writing.\n";
}

int main_shared_mutex() {
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(read_data_sh, i);
    }

    std::thread writer1(write_data_sh, 1, "Data from Writer 1");
    std::thread writer2(write_data_sh, 2, "Data from Writer 2");

    for (std::thread& t : readers) {
        t.join();
    }
    writer1.join();
    writer2.join();

    return 0;
}

6. 自旋锁(Spinlock):忙等待的锁

自旋锁是一种特殊的互斥锁,当线程尝试获取锁而锁已被占用时,它不会立即阻塞,而是会循环检查锁是否可用,直到获取到锁。它适用于临界区非常小、加锁时间极短的场景。

原理:

自旋锁通过忙等待(busy-waiting)来避免线程上下文切换的开销。它通常基于原子操作(如CAS)实现。

C++标准库中没有直接提供std::spinlock,但可以通过std::atomic_flag实现一个简单的自旋锁:

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

// 简单的自旋锁实现
class Spinlock {
public:
    void lock() {
        // test_and_set 如果 atomic_flag 之前是 false,则设置为 true 并返回 false
        // 如果之前是 true,则设置为 true 并返回 true
        // 循环直到成功将 flag 从 false 设置为 true (即获取到锁)
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 忙等待,可以加入 std::this_thread::yield() 优化,避免过度消耗CPU
            // std::this_thread::yield(); 
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release); // 释放锁,将 flag 设置为 false
    }

private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT; // 初始化为 false
};

Spinlock spin_lock_example;
long long global_counter_spinlock = 0;

void increment_counter_spinlock() {
    for (int i = 0; i < 100000; ++i) {
        spin_lock_example.lock();
        global_counter_spinlock++;
        spin_lock_example.unlock();
    }
}

int main_spinlock() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_counter_spinlock);
    }

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

    std::cout << "Final global_counter (spinlock): " << global_counter_spinlock << std::endl;
    return 0;
}

使用场景:

  • 临界区极短:加锁时间比线程上下文切换时间还短的场景。
  • 内核态:操作系统内核中常用,因为内核态上下文切换开销更大。

优缺点:

  • 优点:避免了上下文切换开销,在竞争不激烈且临界区极短时性能优异。
  • 缺点:
    • 忙等待:在多核CPU上,如果锁被长时间占用,会持续消耗CPU资源,造成CPU浪费。
    • 不公平:无法保证公平性,可能导致某些线程饥饿。
    • 不适用于单核CPU:在单核CPU上,自旋锁会导致死锁,因为持有锁的线程无法释放CPU。

三、原子操作与内存模型:细粒度同步与并发秩序

互斥量和条件变量是重量级的同步机制。在某些场景下,我们只需要对单个变量进行原子操作,或者需要更细粒度地控制内存访问顺序,这时可以使用原子操作和内存模型。

1. 原子操作(Atomic Operations):不可分割的操作

原子操作是指在多线程环境下,一个操作要么完全执行,要么完全不执行,不会被任何其他线程的操作中断。C++11引入了std::atomic模板类,提供了对基本数据类型的原子操作支持。

原理:

原子操作通常通过硬件指令(如CPU的CAS指令)或编译器优化来实现,确保操作的不可中断性。这意味着即使在多核处理器上,对std::atomic变量的读写操作也是原子的,不会发生数据竞争。

std::atomic<T>

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

std::atomic<int> atomic_val(0); // 原子变量

void increment_atomic_val() {
    for (int i = 0; i < 100000; ++i) {
        atomic_val++; // 原子递增操作,等价于 atomic_val.fetch_add(1)
    }
}

int main_atomic_basic() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment_atomic_val);
    }

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

    std::cout << "Final atomic_val: " << atomic_val << std::endl; // 预期输出 1000000
    return 0;
}

比较-交换(CAS)操作:compare_exchange_weakcompare_exchange_strong

std::atomic还提供了CAS操作,这是实现无锁(Lock-Free)数据结构的基础。

  • compare_exchange_weak:可能发生虚假失败(Spurious Failure),即即使值相等也可能返回false,通常用于循环中。
  • compare_exchange_strong:保证只有在值不相等时才返回false
cpp
#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> current_value(0);

void update_value_cas(int new_val) {
    int expected = current_value.load(); // 读取当前值
    // 循环直到成功将 current_value 从 expected 更新为 new_val
    while (!current_value.compare_exchange_weak(expected, new_val)) {
        // 如果交换失败,expected 会被自动更新为 current_value 的最新值,继续尝试
        std::cout << "[Thread ID: " << std::this_thread::get_id() 
                  << "] CAS failed, expected: " << expected 
                  << ", current: " << current_value.load() << ", retrying...\n";
    }
    std::cout << "[Thread ID: " << std::this_thread::get_id() 
              << "] Successfully updated value to " << new_val << std::endl;
}

int main_atomic_cas() {
    std::thread t1(update_value_cas, 10);
    std::thread t2(update_value_cas, 20);

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

    std::cout << "Final value (CAS): " << current_value.load() << std::endl;
    return 0;
}

使用场景:

  • 计数器与标志位:实现线程安全的计数器和布尔标志。
  • 无锁数据结构:构建高性能的无锁队列、栈等数据结构。

注意:无锁编程非常复杂且容易出错,通常只在对性能有极致要求且经过严格测试的场景下使用。

2. 内存模型(Memory Model):并发的秩序

C++内存模型定义了在多线程环境下,对内存操作的可见性和顺序性规则。它解决了编译器优化和CPU乱序执行可能导致的问题,确保不同线程对共享内存的访问行为符合预期。

内存序(Memory Order):

std::atomic操作可以指定不同的内存序,以控制操作的可见性和顺序性。理解内存序是理解并发底层行为的关键。

  • memory_order_relaxed:最宽松的内存序,只保证原子性,不保证任何同步或顺序。适用于不依赖其他操作结果的计数器。
  • memory_order_acquire:获取语义。读操作,保证该操作及之后的所有读写操作不会被重排到该操作之前。它“获取”了在它之前所有release操作的内存效果。
  • memory_order_release:释放语义。写操作,保证该操作及之前的所有读写操作不会被重排到该操作之后。它“释放”了在它之前所有操作的内存效果,使其对acquire操作可见。
  • memory_order_acq_rel:获取-释放语义。兼具获取和释放的特性,用于读-改-写操作。
  • memory_order_seq_cst:顺序一致性。最严格的内存序,保证所有线程看到的操作顺序一致。这是std::atomic的默认内存序,也是最安全但开销最大的选项。

示例:Acquire-Release同步

cpp
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<bool> flag(false);
std::atomic<int> value(0);

void producer_memory_order() {
    value.store(100, std::memory_order_release); // 释放语义:保证此前的写入(value=100)对消费者可见
    flag.store(true, std::memory_order_release);  // 释放语义:标记数据已准备好
    std::cout << "[Producer] Data and flag set.\n";
}

void consumer_memory_order() {
    while (!flag.load(std::memory_order_acquire)) { // 获取语义:保证此后的读取能看到生产者释放的内存效果
        std::this_thread::yield(); // 避免忙等待,让出CPU
    }
    std::cout << "[Consumer] Flag is true, value = " << value.load(std::memory_order_acquire) << std::endl;
}

int main_memory_order() {
    std::thread t_prod(producer_memory_order);
    std::thread t_cons(consumer_memory_order);

    t_prod.join();
    t_cons.join();
    return 0;
}

理解内存模型的意义:

内存模型是并发编程中非常高级且复杂的概念。正确使用内存序可以避免不必要的同步开销,但错误使用则可能导致难以调试的并发错误。对于大多数应用,使用默认的顺序一致性(memory_order_seq_cst)通常是安全的。只有在对性能有极致要求时,才需要深入理解并谨慎使用其他内存序。

四、异步任务与并发工具:更高级的抽象

除了直接管理线程和使用同步原语外,C++标准库还提供了一些更高级的并发抽象,它们可以帮助我们更方便地执行异步任务和管理并发操作的结果。

1. std::async:异步执行的便捷之道

std::async是一个函数模板,它可以在后台异步地执行一个函数,并返回一个std::future对象,通过这个std::future对象可以获取异步操作的结果。

原理:

std::async可以根据指定的启动策略(Launch Policy)来决定是立即创建一个新线程执行任务,还是延迟到std::future对象被访问时才执行。它简化了线程的创建和管理,并提供了获取返回值的机制。

启动策略:

  • std::launch::async:强制在新线程中异步执行任务。
  • std::launch::deferred:延迟执行任务,直到std::futureget()wait()被调用时才在当前线程执行。
  • std::launch::async | std::launch::deferred (默认):由C++运行时库决定。
cpp
#include <iostream>
#include <future>
#include <chrono>

// 模拟耗时任务
int heavy_computation(int a, int b) {
    std::cout << "[Task] Calculating...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return a + b;
}

int main_async_example() {
    std::cout << "[Main Thread] Starting async task.\n";

    // 默认启动策略,可能异步也可能延迟
    std::future<int> result_fut = std::async(heavy_computation, 10, 20);

    std::cout << "[Main Thread] Doing other work...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 获取异步任务的结果,get()会阻塞直到结果可用
    int sum = result_fut.get(); 
    std::cout << "[Main Thread] Async task result = " << sum << std::endl;

    // 强制异步执行
    std::future<int> forced_async_fut = std::async(std::launch::async, heavy_computation, 30, 40);
    std::cout << "[Main Thread] Forced async task result = " << forced_async_fut.get() << std::endl;

    // 延迟执行
    std::future<int> deferred_fut = std::async(std::launch::deferred, heavy_computation, 50, 60);
    std::cout << "[Main Thread] Deferred task created. Will execute on get().\n";
    std::cout << "[Main Thread] Deferred task result = " << deferred_fut.get() << std::endl; // 此时才执行

    std::cout << "[Main Thread] All async tasks finished.\n";
    return 0;
}

2. std::packaged_task:任务与结果的分离

std::packaged_task是一个模板类,它将一个可调用对象包装起来,并将其结果存储在一个std::future对象中。它允许你将任务与未来的结果分离,并手动控制任务的执行。

原理:

std::packaged_task封装了一个任务,并提供get_future()方法获取关联的std::future。你可以将std::packaged_task对象传递给std::thread或线程池来执行。

cpp
#include <iostream>
#include <future>
#include <thread>

int calculate_factorial(int n) {
    int res = 1;
    for (int i = 1; i <= n; ++i) {
        res *= i;
    }
    return res;
}

int main_packaged_task() {
    // 1. 创建一个 packaged_task,封装 calculate_factorial 函数
    std::packaged_task<int(int)> task(calculate_factorial);

    // 2. 获取与任务关联的 future 对象
    std::future<int> result_fut = task.get_future();

    // 3. 将任务移动到一个新线程中执行
    std::thread t(std::move(task), 5); // 任务被移动到新线程

    // 4. 等待线程完成
    t.join();

    // 5. 从 future 中获取结果
    std::cout << "Factorial of 5 is: " << result_fut.get() << std::endl; // 输出 120

    return 0;
}

使用场景:

  • 线程池:将任务封装后放入队列,由工作线程取出执行。
  • 异步任务管理:需要将任务和其结果的获取分离,并手动控制任务执行方式时。

3. std::futurestd::promise:异步结果的传递

std::futurestd::promise是C++11中用于在不同线程之间传递异步操作结果的机制。std::promise用于设置一个值或异常,而std::future用于获取这个值或异常。

原理:

std::promise是“生产者”端,它可以在某个时刻设置一个值或异常。std::future是“消费者”端,它可以在稍后获取这个值或异常。当std::promise设置值时,std::future就变为“就绪”状态,等待std::future的线程会被唤醒。

cpp
#include <iostream>
#include <future>
#include <thread>

void producer_promise_future(std::promise<int>&& prom) {
    try {
        // 模拟一些计算
        int result = 123;
        // 设置结果到 promise
        prom.set_value(result);
        std::cout << "[Producer] Result set to " << result << std::endl;
    } catch (...) {
        // 如果发生异常,设置异常到 promise
        prom.set_exception(std::current_exception());
    }
}

void consumer_promise_future(std::future<int>&& fut) {
    try {
        // 从 future 获取结果,get() 会阻塞直到结果可用
        int value = fut.get();
        std::cout << "[Consumer] Received result = " << value << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "[Consumer] Caught exception: " << e.what() << std::endl;
    }
}

int main_promise_future() {
    // 创建 promise 对象
    std::promise<int> prom;

    // 获取与 promise 关联的 future 对象
    std::future<int> fut = prom.get_future();

    // 启动生产者线程,将 promise 移动给它
    std::thread prod_t(producer_promise_future, std::move(prom));

    // 启动消费者线程,将 future 移动给它
    std::thread cons_t(consumer_promise_future, std::move(fut));

    prod_t.join();
    cons_t.join();

    std::cout << "[Main Thread] Promise/Future example finished.\n";

    // 演示异常传递
    std::promise<void> prom_ex;
    std::future<void> fut_ex = prom_ex.get_future();

    std::thread t_prod_ex([&]() {
        try {
            throw std::runtime_error("Something went wrong in producer!");
        } catch (...) {
            prom_ex.set_exception(std::current_exception());
        }
    });

    std::thread t_cons_ex([&]() {
        try {
            fut_ex.get();
        } catch (const std::exception& e) {
            std::cerr << "[Consumer (Exception)] Caught exception: " << e.what() << std::endl;
        }
    });

    t_prod_ex.join();
    t_cons_ex.join();

    return 0;
}

使用场景:

  • 线程间结果传递:一个线程计算出结果后,传递给另一个等待该结果的线程。
  • 异步操作的返回值:作为std::asyncstd::packaged_task的底层机制。
  • 实现一次性事件:当某个事件发生后,通知所有等待的线程。

五、总结与最佳实践:驾驭并发的艺术

C++多线程编程是现代软件开发中不可或缺的技能,它能够帮助我们充分利用多核处理器的优势,构建高性能、高响应的应用程序。然而,并发编程也充满了挑战,需要开发者对线程的生命周期、数据同步、内存模型以及高级并发工具都有深入的理解。

多线程编程的常见挑战:

  • 数据竞争(Data Race):多个线程同时访问和修改共享数据,导致结果不可预测。
  • 死锁(Deadlock):多个线程相互等待对方释放资源,导致所有线程都无法继续执行。
  • 活锁(Livelock):线程不断地尝试获取资源,但由于其他线程的干扰而一直失败,导致无法完成任务。
  • 饥饿(Starvation):某个线程由于优先级低或资源分配不公平,长时间无法获取所需资源而无法执行。
  • 性能问题:过度同步可能导致性能下降,而同步不足则可能导致正确性问题。

C++多线程编程最佳实践:

  1. 优先使用高级抽象:尽可能使用std::asyncstd::packaged_taskstd::future等高级并发工具,它们简化了线程管理和结果获取,减少了出错的可能性。
  2. 谨慎使用互斥量:
    • RAII原则:始终使用std::lock_guardstd::unique_lock来管理互斥量,确保锁的正确释放。
    • 最小化临界区:只在真正需要保护共享数据时才锁定互斥量,并且临界区应尽可能小,以提高并发度。
    • 固定加锁顺序:如果需要获取多个互斥量,所有线程都应遵循相同的加锁顺序,或使用std::lock()来避免死锁。
  3. 正确使用条件变量:
    • 始终与互斥量配合:条件变量必须与互斥量一起使用。
    • 检查条件:wait()方法必须配合Lambda表达式进行条件检查,以避免虚假唤醒。
  4. 理解内存模型:对于高性能场景或无锁编程,需要深入理解C++内存模型和不同的内存序,但对于大多数应用,默认的顺序一致性是安全的。
  5. 避免共享状态:最好的同步是避免同步。如果可能,尽量设计无共享状态的线程,或者通过消息传递等方式进行通信,而不是直接共享内存。
  6. 异常安全:确保多线程代码在发生异常时能够正确释放资源,避免死锁或资源泄露。
  7. 测试与调试:并发问题往往难以复现和调试,需要充分的测试和借助专业的并发调试工具。

常见问题

Q1: std::threadstd::async有什么区别?

std::thread是更底层的线程管理工具,你需要手动管理线程的生命周期(join()detach()),并且无法直接获取线程函数的返回值。std::async是更高级的抽象,它会自动管理线程的生命周期(通过std::future的析构),并且可以方便地获取异步任务的返回值或异常。对于简单的异步任务,std::async通常是更推荐的选择。

Q2: 什么是虚假唤醒(Spurious Wakeup)?如何避免?

虚假唤醒是指条件变量的wait()方法在没有收到notify_one()notify_all()通知的情况下被唤醒。这是操作系统调度的一种优化,是允许发生的。为了避免因此导致的问题,必须始终在循环中检查条件变量所等待的条件。例如:while (!condition) { cv.wait(lock); }cv.wait(lock, []{ return condition; });

Q3: std::mutexstd::atomic有什么区别?什么时候用哪个?

  • std::mutex:是一种互斥锁,用于保护临界区,确保同一时间只有一个线程访问共享资源。它会阻塞线程,涉及上下文切换,开销相对较大。适用于保护复杂数据结构或多个变量的访问。
  • std::atomic:提供对单个变量的原子操作,确保操作的不可分割性。它通常通过硬件指令实现,不会阻塞线程,开销较小。适用于对单个变量(如计数器、标志位)的简单、频繁操作,或构建无锁数据结构。

选择:对于复杂的数据结构或需要保护多个相关变量的场景,使用std::mutex。对于单个变量的简单原子操作,且对性能有较高要求时,可以考虑std::atomic

Q4: 什么是内存屏障(Memory Barrier)?

内存屏障(或内存栅栏)是一种CPU指令,用于强制CPU或编译器在执行特定操作时遵循严格的顺序。它确保在屏障之前的内存操作在屏障之后的内存操作之前完成。在C++中,内存屏障的概念通过std::atomic的内存序(memory_order)来体现和控制,例如memory_order_acquirememory_order_release就隐含了内存屏障的作用。

Q5: 如何避免多线程中的饥饿问题?

饥饿问题通常发生在资源分配不公平或优先级反转的情况下。避免饥饿的策略包括:

  • 公平锁:使用支持公平性策略的锁(C++标准库的锁默认不保证公平性,但有些第三方库提供)。
  • 优先级继承/反转协议:在实时系统中,通过优先级继承或优先级天花板协议来解决优先级反转问题。
  • 避免长时间持有锁:尽量缩短临界区,减少线程等待时间。
  • 资源轮询或超时机制:使用try_lock_for等带超时机制的锁,避免线程无限期等待。

希望这篇博文能帮助您深入理解C++多线程编程,并在您的软件开发旅程中提供有益的指导。祝您在构建高性能、高并发的C++应用道路上越走越远!