多线程编程基础:线程的创建与管理、互斥锁、条件变量、原子操作

说实话,多线程编程是C++里一个让人又爱又恨的话题。爱它,是因为它能榨干CPU的每一滴性能;恨它,是因为稍不留神就踩坑——数据竞争、死锁、活锁,哪个都不好惹。我刚开始接触多线程时,也犯过不少低级错误,比如忘记加锁导致两个线程同时修改同一个变量,结果程序跑着跑着就崩了。

这一章,咱们就聊聊C++11标准引入的多线程库。它提供了std::threadstd::mutexstd::condition_variablestd::atomic这些核心工具。掌握了它们,你就能写出安全又高效的多线程程序。

1. 线程的创建与管理:std::thread

std::thread是C++多线程的基石。它代表一个独立的执行线程。创建线程很简单,传一个可调用对象(函数、lambda、函数对象)给它就行。

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(hello);  // 创建线程并启动
    t.join();              // 等待线程结束
    return 0;
}

这里有个细节:t.join()会阻塞主线程,直到子线程执行完毕。如果你不调用join()detach(),程序会在std::thread析构时调用std::terminate()——直接崩溃。嗯,这算是个常见的坑。

注意:一定要确保线程对象在销毁前调用了join()detach()。否则程序会异常终止。

我个人习惯用join(),因为它能保证线程执行完毕后再继续。而detach()会让线程在后台运行,主线程不再关心它的死活——这容易导致资源泄漏或访问已销毁的变量。

线程还可以接受参数:

void print_sum(int a, int b) {
    std::cout << a + b << '\n';
}

int main() {
    std::thread t(print_sum, 3, 4);
    t.join();
    return 0;
}

参数会按值传递。如果你想传引用,得用std::ref包装一下,否则编译会报错。

2. 互斥锁:std::mutex

多线程最头疼的问题就是数据竞争。两个线程同时读写同一个变量,结果不可预测。互斥锁就是用来解决这个问题的。

std::mutex提供了lock()unlock()方法。但手动管理锁很容易出错——万一中间抛出异常,锁就永远解不开了。所以C++提供了std::lock_guardstd::unique_lock,它们利用RAII机制自动管理锁的生命周期。

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

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << '\n';
    return 0;
}

你看,lock_guard在构造时自动加锁,析构时自动解锁。就算中间抛出异常,锁也会被正确释放。我在项目中遇到过有人手动lock()unlock(),结果漏了一个unlock(),整个程序就卡死了。所以,能用RAII就别手动管理。

小技巧:如果需要在锁的作用域内手动控制锁的释放(比如配合条件变量),用std::unique_lock。它比lock_guard更灵活,但性能稍差一点。

3. 条件变量:std::condition_variable

条件变量用于线程间的同步。一个线程等待某个条件成立,另一个线程在条件满足时通知它。典型的场景是生产者-消费者模型。

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool done = false;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::lock_guard<std::mutex> lock(mtx);
        data_queue.push(i);
        cv.notify_one();  // 通知消费者
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        done = true;
    }
    cv.notify_one();
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !data_queue.empty() || done; });
        while (!data_queue.empty()) {
            int val = data_queue.front();
            data_queue.pop();
            lock.unlock();
            std::cout << "Consumed: " << val << '\n';
            lock.lock();
        }
        if (done) break;
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

这里的关键是cv.wait()。它接受一个锁和一个谓词(lambda)。线程会先释放锁,然后阻塞自己,直到被唤醒且谓词返回true。为什么要用谓词?因为notify_one()可能唤醒多个线程,或者有虚假唤醒(spurious wakeup)。谓词能确保线程只在真正满足条件时才继续执行。

注意:条件变量必须配合std::unique_lock使用,因为wait()需要在等待期间释放锁,并在返回前重新获取锁。lock_guard做不到这一点。

我曾经写过一个bug:在wait()之前忘了检查条件,结果线程被唤醒后数据还没准备好,直接访问了空队列。加上谓词后,问题就解决了。

4. 原子操作:std::atomic

互斥锁能解决数据竞争,但加锁解锁有性能开销。如果只是对单个变量做简单的读写或加减操作,可以用原子类型。原子操作是CPU直接支持的,比锁快得多。

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

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

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << '\n';
    return 0;
}

std::atomic<int>保证对counter的所有操作都是原子的。你不需要加锁,也不用担心数据竞争。不过要注意,原子操作只对单个变量有效。如果你需要同时修改多个变量,还是得用互斥锁。

原子操作还有一个重要概念:内存序(memory order)。它控制着原子操作之间的可见性和顺序。常用的有:

内存序 说明
memory_order_relaxed 只保证原子性,不保证顺序。性能最好,但容易出错。
memory_order_acquire 保证后续的读操作不会被重排到当前操作之前。
memory_order_release 保证之前的写操作不会被重排到当前操作之后。
memory_order_seq_cst 最严格,全局顺序一致。性能最差,但最安全。

我个人建议:如果你不确定用哪个,就用默认的memory_order_seq_cst。虽然慢一点,但至少不会出错。等你对内存模型有更深理解后,再考虑优化。

核心要点:
  • std::thread用于创建和管理线程,记得调用join()detach()
  • std::mutex配合std::lock_guardstd::unique_lock使用,避免手动管理锁。
  • std::condition_variable用于线程间等待和通知,必须配合std::unique_lock
  • std::atomic提供无锁的原子操作,适合简单的共享变量。

多线程编程就像走钢丝,平衡好了性能和安全,你就能写出高效的程序。但别急,先把这些基础打牢。下一章咱们会深入聊聊线程池和异步任务,那又是另一番天地了。

多线程编程核心知识体系 多线程编程 std::thread std::mutex std::condition_variable std::atomic join() / detach() lock_guard / unique_lock wait() / notify_one() fetch_add / memory_order
我的建议:刚开始学多线程,别急着优化。先用std::mutexstd::lock_guard把程序写对,再考虑用原子操作或更复杂的内存序。性能问题可以等程序跑起来后再用工具分析。

好了,这一章的内容就到这里。多线程编程是个大话题,但核心就是这几个工具。你想想看,线程负责干活,互斥锁负责保护数据,条件变量负责协调,原子操作负责快速处理简单变量。把它们用好,大部分多线程问题都能搞定。