C++中的thread

C++ 的多线程支持是从 C++11 标准开始引入的,标准库提供了 <thread> 头文件,使得多线程编程变得跨平台且更加简便。在此之前,开发者通常需要依赖操作系统特定的 API(如 Linux 的 pthreads 或 Windows 的 CreateThread)。

1. std::thread 的基础使用

要使用多线程,首先需要包含头文件:

复制代码
#include <thread>
创建与启动线程

std::thread 对象一旦创建,线程就会立即开始执行。你可以传递函数指针Lambda 表达式或**函数对象(Functor)**作为线程的入口。

复制代码
#include <iostream>
#include <thread>

void worker(int n) {
    std::cout << "Thread " << n << " is running." << std::endl;
}

int main() {
    // 创建一个线程 t1,执行 worker 函数,传入参数 1
    std::thread t1(worker, 1);

    // 使用 Lambda 表达式创建线程
    std::thread t2([](){
        std::cout << "Lambda thread is running." << std::endl;
    });

    // 等待线程结束(必须操作,否则程序退出时会崩溃)
    t1.join();
    t2.join();

    std::cout << "Main thread finished." << std::endl;
    return 0;
}

2. 核心成员函数:joindetach

创建线程后,必须决定主线程(或父线程)如何处理这个新线程。这是多线程编程中最基本也是最重要的步骤。

|--------------|-------------------------------------------------------------------------------|-------------------------------------------------|
| 函数 | 描述 | 比喻 |
| join | 阻塞等待。主线程停下来,等待子线程执行完毕后,汇合(join)到一起,然后再继续执行。 | 父母等孩子:父母(主线程)必须等孩子(子线程)吃完饭,大家才能一起离席。 |
| detach | 分离运行。子线程与主线程断开联系,在后台独立运行。主线程结束也不影响子线程(除非整个进程结束)。 | 放风筝断线:风筝(子线程)飞走了,父母(主线程)不再管它,它自己飞直到落地(执行完)。 |
| joinable | 检查线程是否可以被 joindetach 。如果一个线程已经被 join 过或 detach 过,它就不再是 joinable 的。 | 检查孩子是否还在饭桌上。 |

注意: 如果 std::thread 对象在销毁前(例如离开作用域)没有调用 join() 也没有调用 detach(),程序会调用 std::terminate() 导致崩溃。


3. 线程传参:值拷贝 vs 引用

这是 C++ 多线程编程中极易出错的地方。

  • 默认行为: std::thread 构造函数会拷贝所有的参数到线程的独立内存空间中。

  • 传递引用: 如果你想在线程中修改外部变量,或者为了性能不想拷贝(例如大对象),必须使用 std::ref() 显式包装引用。

    #include <thread>
    #include <iostream>

    void updateValue(int& n) {
    n += 10;
    }

    int main() {
    int val = 0;

    复制代码
      // 错误用法:std::thread t(updateValue, val); // 编译报错,或者产生拷贝导致 val 不变
    
      // 正确用法:使用 std::ref
      std::thread t(updateValue, std::ref(val));
    
      t.join();
      std::cout << "Value is: " << val << std::endl; // 输出 10
      return 0;

    }


4. 线程同步与数据竞争 (Synchronization)

当多个线程同时访问同一个变量(且至少有一个是写操作)时,就会发生数据竞争 (Data Race),导致结果不可预测。

为了解决这个问题,我们需要使用互斥锁 (Mutex)

A. 互斥量 (std::mutex)

位于 <mutex> 头文件中。

  • lock(): 上锁。如果已被锁,则阻塞等待。
  • unlock(): 解锁。
B. RAII 锁管理 (std::lock_guardstd::unique_lock)

强烈建议不要直接手动调用 lock()** unlock(),因为如果中间抛出异常或忘记解锁,会导致死锁 (Deadlock)**。应该使用 RAII 风格的封装类。

  • std::lock_guard: 最简单,构造时上锁,析构时(作用域结束)自动解锁。不可手动解锁。

  • std::unique_lock: 更灵活,可以手动 lock/unlock,可以延迟锁定,通常配合条件变量使用。

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

    int shared_counter = 0;
    std::mutex mtx; // 全局互斥锁

    void increase() {
    for (int i = 0; i < 1000; ++i) {
    // 进入作用域自动上锁
    std::lock_guardstd::mutex lock(mtx);

    复制代码
          // 临界区 (Critical Section)
          shared_counter++; 
    
          // 离开作用域,lock 自动析构并解锁
      }

    }

    int main() {
    std::vectorstd::thread threads;
    for(int i=0; i<10; ++i) {
    threads.emplace_back(increase);
    }

    复制代码
      for(auto& t : threads) t.join();
    
      std::cout << "Final counter: " << shared_counter << std::endl; // 必定是 10000
      return 0;

    }


5. 进阶:原子操作 (std::atomic)

对于简单的变量(如 int, bool)的自增、自减或状态标记,使用互斥锁太重了(开销大)。C++ 提供了 <atomic>

std::atomic 保证了对变量的操作是原子的(不可分割),无需加锁,性能更高。

复制代码
#include <atomic>

std::atomic<int> atom_counter(0); // 定义原子整型

void fastIncrease() {
    for (int i = 0; i < 1000; ++i) {
        atom_counter++; // 这一步是原子的,线程安全
    }
}

6. 多线程编程的常见陷阱

  1. 死锁 (Deadlock)
    • 场景:线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1。两者互相等待,永久卡死。
    • 解决 :保证所有线程以相同的顺序获取锁;或者使用std::lock(mtx1, mtx2) 同时锁定多个互斥量。
  1. 悬空引用 (Dangling Reference)
    • 场景 :你在函数中创建了一个线程,并传递了一个局部变量的引用给它。然后使用了 detach()--->主线程和子线程分离。
    • 后果 :如果主函数先结束,局部变量被销毁,但子线程还在运行并试图访问那个已经被销毁的变量 -> 崩溃
  1. 虚假唤醒 (Spurious Wakeup)
    • 在使用 std::condition_variable(条件变量)等待信号时,线程可能会在没有收到信号的情况下醒来。因此,wait 必须放在 while 循环中检查条件,而不是 if
相关推荐
qq_479875432 小时前
C++ 鸭子类型” (Duck Typing)
开发语言·c++
崇山峻岭之间2 小时前
C++ Prime Plus 学习笔记033
c++·笔记·学习
暗然而日章2 小时前
C++基础:Stanford CS106L学习笔记 7 类
c++·笔记·学习
释怀°Believe2 小时前
Daily算法刷题【面试经典150题-5️⃣图】
算法·面试·深度优先
思成不止于此3 小时前
【MySQL 零基础入门】DDL 核心语法全解析:数据库与表结构操作篇
数据库·笔记·学习·mysql
lkbhua莱克瓦243 小时前
Java进阶——IO流
java·开发语言·笔记·学习方法·io流
浦东新村轱天乐3 小时前
2025.12.01-2025.12.07:休假回来,开始迭代vlm
笔记
im_AMBER3 小时前
Leetcode 72 数组列表中的最大距离
c++·笔记·学习·算法·leetcode
FFF团团员9093 小时前
树莓派学习笔记7:局域网的建立和程序自启动
笔记·学习