C++ 多线程库 <thread>(长文解析)

C++ 多线程库 入门指南

在现代编程中,多任务处理早已不是“高级功能”,而是开发高性能应用的标配。无论是图像处理、网络请求,还是数据计算,单线程程序常常因为“卡顿”或“效率低下”而难以满足需求。C++ 从 C++ 11 起正式引入了标准的多线程支持,其中最核心的组件就是 <thread> 库。它为我们提供了一种跨平台、安全且高效的线程管理方式。

如果你正在学习 C++,或者已经掌握基础语法,但对并发编程感到陌生,那么这篇教程将带你一步步理解 C++ 多线程库 的使用方式与核心思想。我们不会一开始就讲复杂的同步机制,而是从最简单的线程创建开始,逐步深入。


创建与启动线程

在 C++ 中,std::thread 是创建线程的入口类。它就像一个“线程工厂”,你告诉它“我要执行什么函数”,它就会为你创建一个独立的执行流。

下面是一个最简单的例子:

#include <iostream>
#include <thread>

// 定义一个普通的函数,作为线程的入口
void print_hello() {
    for (int i = 0; i < 5; ++i) {
        std::cout << "Hello from thread " << i << std::endl;
        // 模拟耗时操作
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    // 创建一个线程对象,传入函数名作为参数
    std::thread t(print_hello);

    // 主线程继续执行
    std::cout << "Main thread is running..." << std::endl;

    // 等待子线程结束,防止主程序提前退出
    t.join();

    std::cout << "All threads finished." << std::endl;

    return 0;
}

代码注释说明:

  • std::thread t(print_hello);:创建一个新线程,执行 print_hello 函数。注意这里只传函数名,不加括号,否则会直接调用。
  • std::this_thread::sleep_for(...):让当前线程暂停指定时间,常用于模拟任务耗时,避免输出过快。
  • t.join();:主线程等待子线程结束。这是关键!如果没有 join(),主程序可能在子线程完成前就退出,导致子线程未执行完毕就被强制终止。

⚠️ 提示:join() 不能重复调用,否则程序会崩溃。如果不需要等待,可以使用 detach(),但要小心资源泄漏。


线程间传递参数

很多时候,我们希望线程执行时能接收参数。std::thread 支持传递任意类型参数,包括基本类型、结构体、甚至智能指针。

#include <iostream>
#include <thread>
#include <string>

// 接收两个参数的函数
void greet(const std::string& name, int times) {
    for (int i = 0; i < times; ++i) {
        std::cout << "Hello, " << name << "! (iteration " << i + 1 << ")" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    // 传递字符串和整数参数
    std::thread t(greet, "Alice", 3);

    std::cout << "Main thread continues..." << std::endl;

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

    std::cout << "Greeting thread finished." << std::endl;

    return 0;
}

关键点:

  • std::thread t(greet, "Alice", 3);:参数通过值传递,"Alice"3 会被复制到子线程中。
  • 如果你希望传递引用,必须使用 std::ref() 包装,否则会出错。
int counter = 0;

void increment(int& count) {
    for (int i = 0; i < 5; ++i) {
        ++count;
        std::cout << "Count: " << count << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::thread t(increment, std::ref(counter));  // 使用 std::ref 传递引用
    t.join();
    std::cout << "Final count: " << counter << std::endl;
    return 0;
}

💡 小贴士:std::ref() 是一个工具函数,用于包装引用,让线程能共享变量。如果不加,编译器会报错,因为引用不能被复制。


线程生命周期管理:join 与 detach

线程一旦创建,就拥有自己的生命周期。我们有两种方式来管理它:

方法 说明 适用场景
join() 等待线程结束,阻塞当前线程 主线程需要等待子线程完成
detach() 让线程独立运行,脱离主线程控制 子线程执行后台任务,无需等待

使用 detach() 时必须特别小心,因为一旦线程脱离控制,你将无法再调用 join(),也无法保证它安全退出。

#include <iostream>
#include <thread>

void background_task() {
    for (int i = 0; i < 10; ++i) {
        std::cout << "Background task: " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(150));
    }
    std::cout << "Background task finished." << std::endl;
}

int main() {
    std::thread t(background_task);

    // 让线程脱离主线程
    t.detach();

    std::cout << "Main thread continues without waiting." << std::endl;

    // 主线程退出,子线程仍会继续运行
    // 注意:这里不能调用 t.join(),否则程序崩溃

    // 等待一段时间,确保后台任务完成
    std::this_thread::sleep_for(std::chrono::seconds(2));

    return 0;
}

⚠️ 危险警告:如果 main() 提前退出,而 detach() 的线程尚未完成,程序可能异常终止。建议在 detach() 后添加适当的等待机制或使用 RAII 模式管理。


线程唯一性与资源安全

每个 std::thread 对象都唯一对应一个线程。这意味着你不能复制 std::thread 对象,但可以移动。

#include <iostream>
#include <thread>

void worker() {
    std::cout << "Worker thread running." << std::endl;
}

int main() {
    std::thread t1(worker);

    // 错误:不能复制 thread
    // std::thread t2 = t1;  // 编译错误!

    // 正确:使用移动语义
    std::thread t2 = std::move(t1);

    // t1 现在无效,不能再 join 或 detach
    if (t1.joinable()) {
        t1.join();
    } else {
        std::cout << "t1 is not joinable after move." << std::endl;
    }

    t2.join();

    return 0;
}

关键概念:

  • joinable():判断线程是否可以被 join()detach()
  • std::move():将线程的所有权从一个对象转移到另一个,是唯一合法的线程转移方式。

比喻:std::thread 就像一个“许可证”,一个许可证只能给一个线程使用。复制相当于“伪造许可证”,是非法的;而移动,就像“转让许可证”,是合法且安全的。


线程安全与共享数据保护

当多个线程访问同一个变量时,就可能出现“竞态条件”(Race Condition)。例如两个线程同时对 counter 加 1,结果可能只加了 1 次。

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

int counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;  // 竞态条件!
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建 10 个线程
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

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

    std::cout << "Final counter value: " << counter << std::endl;

    return 0;
}

运行结果可能是 99999000,而不是预期的 10000。这是由于多个线程同时读写 counter,导致操作被覆盖。

解决方案:使用互斥锁(std::mutex

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

int counter = 0;
std::mutex mtx;  // 互斥锁

void increment() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 自动加锁/解锁
        ++counter;
    }
}

int main() {
    std::vector<std::thread> threads;

    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

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

    std::cout << "Final counter value: " << counter << std::endl;  // 输出 10000

    return 0;
}

核心思想:

  • std::lock_guard 是一个 RAII 机制,进入作用域时自动加锁,离开时自动解锁。
  • 任何时刻只有一个线程能持有锁,从而保证 ++counter 操作的原子性。

总结:C++ 多线程库 的核心价值

C++ 多线程库 不仅仅是一个“创建线程”的工具,它构建了一整套并发编程的基础框架。从线程创建、参数传递,到生命周期管理,再到线程安全保护,每一个环节都设计得既强大又安全。

对于初学者,建议从 std::thread + join() 开始,逐步理解线程的执行流程;中级开发者则应深入学习 std::mutexstd::condition_variable 等同步原语,以构建更复杂的并发模型。

记住:多线程不是“让程序跑得更快”的万能药,而是需要精心设计的工具。错误的并发使用,反而会导致性能下降、死锁甚至崩溃。

掌握 C++ 多线程库 ,是你迈向高性能 C++ 开发的重要一步。从今天起,别再让程序“串行”运行了,让多线程为你加速!