C++ `std::thread` 深度解析:线程的生命周期管理与常见陷阱

C++ std::thread 深度解析:线程的生命周期管理与常见陷阱

大家好!今天咱们来聊聊C++里一个既强大又容易让人踩坑的家伙——std::thread。这玩意儿能让你程序里同时跑多个任务,听起来是不是很酷?但要是对它的生命周期和一些常见陷阱不了解,那可就等着被它坑惨吧!

线程的创建与启动:让你的程序“分身术”

std::thread 最基本的功能就是创建并启动一个新的线程。简单来说,就是让你的程序学会“分身术”,同时干好几件事。

基本用法:

#include <iostream>
#include <thread>

void say_hello() {
  std::cout << "Hello from a thread!" << std::endl;
}

int main() {
  std::thread my_thread(say_hello); // 创建线程,执行 say_hello 函数
  my_thread.join(); // 等待线程执行完毕

  std::cout << "Hello from the main thread!" << std::endl;
  return 0;
}

这段代码创建了一个新的线程,这个线程会执行say_hello函数,打印一句“Hello from a thread!”。my_thread.join()这行代码非常重要,它会让主线程等待新线程执行完毕。如果没有这行,主线程可能会比新线程先结束,导致程序崩溃或者行为异常。

传参给线程函数:

线程函数也可以接收参数,就像普通函数一样。

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

void greet(const std::string& name) {
  std::cout << "Hello, " << name << "! from a thread." << std::endl;
}

int main() {
  std::string my_name = "Alice";
  std::thread my_thread(greet, my_name); // 将 my_name 作为参数传递给 greet 函数
  my_thread.join();

  return 0;
}

注意,传参的时候,C++会默认进行参数拷贝。如果你想传递引用,可以使用 std::ref

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

void increment(int& counter) {
  for (int i = 0; i < 10000; ++i) {
    counter++;
  }
}

int main() {
  int counter = 0;
  std::thread my_thread(increment, std::ref(counter)); // 传递 counter 的引用
  my_thread.join();

  std::cout << "Counter value: " << counter << std::endl;
  return 0;
}

如果没有 std::refincrement 函数接收到的将是 counter 的拷贝,对拷贝的修改不会影响到 main 函数中的 counter。 这在多线程编程中可是个大坑,一定要小心!

使用 Lambda 表达式:

std::thread 还可以和 Lambda 表达式配合使用,让代码更简洁。

#include <iostream>
#include <thread>

int main() {
  int value = 42;
  std::thread my_thread([&value]() { // 使用 Lambda 表达式
    std::cout << "Value from thread: " << value << std::endl;
  });
  my_thread.join();

  return 0;
}

Lambda 表达式可以捕获外部变量,通过 [&value] 捕获 value 的引用,或者通过 [value] 捕获 value 的拷贝。

线程的生命周期管理:joindetach

线程的生命周期管理是使用 std::thread 的关键。线程对象必须在析构之前被 join 或者 detach。否则,程序会直接崩溃!

join():等待线程结束

join() 函数会阻塞调用线程,直到被 join 的线程执行完毕。这就像你等着你的小伙伴完成任务,你才能继续做下一步。

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

void worker() {
  std::cout << "Worker thread started." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
  std::cout << "Worker thread finished." << std::endl;
}

int main() {
  std::thread my_thread(worker);
  std::cout << "Main thread: starting worker thread..." << std::endl;
  my_thread.join(); // 等待 worker 线程执行完毕
  std::cout << "Main thread: worker thread finished." << std::endl;
  return 0;
}

detach():让线程独立运行

detach() 函数会让线程脱离主线程的控制,成为一个独立的线程。这意味着主线程不会等待它结束,程序会继续执行后续代码。这就像你让你的小伙伴自己去玩,你不用管他什么时候回来。

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

void daemon_task() {
  std::cout << "Daemon thread started." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟长时间运行的任务
  std::cout << "Daemon thread finished (maybe)." << std::endl;
}

int main() {
  std::thread daemon(daemon_task);
  daemon.detach(); // 让 daemon 线程独立运行
  std::cout << "Main thread continues execution." << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(1)); // 主线程睡眠一段时间
  std::cout << "Main thread finished." << std::endl;
  return 0;
}

使用 detach() 需要特别小心。因为主线程结束后,detached 线程仍然会继续运行,如果 detached 线程需要访问主线程中的资源(比如变量),可能会导致问题。

joinable():检查线程是否可以 join

在调用 join() 或者 detach() 之前,可以使用 joinable() 函数来检查线程是否可以 join。如果线程已经 join 或者 detach,或者线程对象是默认构造的,joinable() 会返回 false

#include <iostream>
#include <thread>

void task() {
  std::cout << "Task executed." << std::endl;
}

int main() {
  std::thread my_thread(task);

  if (my_thread.joinable()) {
    std::cout << "Thread is joinable." << std::endl;
    my_thread.join();
  } else {
    std::cout << "Thread is not joinable." << std::endl;
  }

  return 0;
}

常见陷阱:一不小心就翻车

std::thread 用起来很爽,但一不小心就会踩到坑。

1. 忘记 join 或者 detach

这是最常见的错误。如果线程对象在析构之前没有被 join 或者 detach,程序会直接崩溃。

#include <iostream>
#include <thread>

void dangerous_task() {
  std::cout << "Dangerous task running..." << std::endl;
}

int main() {
  std::thread my_thread(dangerous_task);
  // 忘记 join 或者 detach,程序崩溃!
  return 0;
}

解决方法: 始终记得在线程对象析构之前调用 join 或者 detach。可以使用 RAII (Resource Acquisition Is Initialization) 技术,将线程对象封装在一个类中,在类的析构函数中自动调用 join 或者 detach

#include <iostream>
#include <thread>

class ThreadGuard {
 public:
  ThreadGuard(std::thread& t) : thread_(t) {}
  ~ThreadGuard() {
    if (thread_.joinable()) {
      std::cout << "Thread still joinable, joining..." << std::endl;
      thread_.join();
    }
  }

 private:
  std::thread& thread_;
};

void safe_task() {
  std::cout << "Safe task running..." << std::endl;
}

int main() {
  std::thread my_thread(safe_task);
  ThreadGuard guard(my_thread); // 使用 RAII 确保线程被 join
  return 0;
}

2. 数据竞争:

多个线程同时访问和修改同一个共享数据,可能会导致数据竞争,产生不可预测的结果。

#include <iostream>
#include <thread>

int shared_data = 0;

void increment() {
  for (int i = 0; i < 100000; ++i) {
    shared_data++; // 数据竞争!
  }
}

int main() {
  std::thread thread1(increment);
  std::thread thread2(increment);

  thread1.join();
  thread2.join();

  std::cout << "Shared data: " << shared_data << std::endl; // 结果可能不是 200000
  return 0;
}

解决方法: 使用互斥锁(std::mutex)或其他同步机制来保护共享数据。

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

int shared_data = 0;
std::mutex mutex; // 互斥锁

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

int main() {
  std::thread thread1(increment);
  std::thread thread2(increment);

  thread1.join();
  thread2.join();

  std::cout << "Shared data: " << shared_data << std::endl; // 结果是 200000
  return 0;
}

3. 死锁:

多个线程互相等待对方释放资源,导致所有线程都无法继续执行。

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

std::mutex mutex1;
std::mutex mutex2;

void task1() {
  mutex1.lock();
  std::cout << "Task 1: acquired mutex1" << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  mutex2.lock(); // 等待 mutex2,可能导致死锁
  std::cout << "Task 1: acquired mutex2" << std::endl;
  mutex2.unlock();
  mutex1.unlock();
}

void task2() {
  mutex2.lock();
  std::cout << "Task 2: acquired mutex2" << std::endl;
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  mutex1.lock(); // 等待 mutex1,可能导致死锁
  std::cout << "Task 2: acquired mutex1" << std::endl;
  mutex1.unlock();
  mutex2.unlock();
}

int main() {
  std::thread thread1(task1);
  std::thread thread2(task2);

  thread1.join();
  thread2.join();

  return 0;
}

解决方法: 避免循环等待,使用 std::lock 同时锁定多个互斥锁,或者使用 std::unique_lockstd::try_lock 来尝试获取锁。

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

std::mutex mutex1;
std::mutex mutex2;

void safe_task1() {
  std::lock(mutex1, mutex2); // 同时锁定两个互斥锁
  std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
  std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);

  std::cout << "Safe Task 1: acquired mutex1 and mutex2" << std::endl;
}

void safe_task2() {
  std::lock(mutex2, mutex1); // 同时锁定两个互斥锁,顺序和 task1 相同
  std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
  std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);

  std::cout << "Safe Task 2: acquired mutex2 and mutex1" << std::endl;
}

int main() {
  std::thread thread1(safe_task1);
  std::thread thread2(safe_task2);

  thread1.join();
  thread2.join();

  return 0;
}

4. 悬挂指针/引用:

Detached 线程访问主线程中已经销毁的变量。

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

void detached_task(int* ptr) {
  std::this_thread::sleep_for(std::chrono::seconds(2));
  if (ptr != nullptr) {
    std::cout << "Value from detached thread: " << *ptr << std::endl; // 可能访问无效内存
  } else {
    std::cout << "Pointer is null." << std::endl;
  }
}

int main() {
  int value = 42;
  int* ptr = &value;
  std::thread my_thread(detached_task, ptr);
  my_thread.detach();

  std::this_thread::sleep_for(std::chrono::seconds(1)); // 主线程先结束
  // value 已经被销毁,detached_task 访问的是无效内存
  return 0;
}

解决方法: 避免让 detached 线程访问主线程中的局部变量。如果必须访问,可以使用智能指针(std::shared_ptr)来管理内存,或者使用线程安全的队列来传递数据。

5. 异常安全:

线程函数抛出异常,如果没有捕获,程序会终止。

#include <iostream>
#include <thread>
#include <stdexcept>

void throwing_task() {
  std::cout << "Throwing task started." << std::endl;
  throw std::runtime_error("Something went wrong!");
  std::cout << "Throwing task finished (never reached)." << std::endl;
}

int main() {
  std::thread my_thread(throwing_task);
  try {
    my_thread.join(); // 抛出异常,程序终止
  } catch (const std::exception& e) {
    std::cerr << "Caught exception: " << e.what() << std::endl;
  }

  return 0;
}

解决方法: 在线程函数中捕获异常,或者使用 RAII 技术,确保在异常情况下也能正确处理线程的生命周期。

总结:std::thread 使用注意事项

陷阱 解决方法
忘记 join/detach 使用 RAII 技术,在对象析构时自动 join 或者 detach
数据竞争 使用互斥锁 (std::mutex)、原子变量 (std::atomic) 等同步机制保护共享数据。
死锁 避免循环等待,使用 std::lock 同时锁定多个互斥锁,或者使用 std::unique_lockstd::try_lock 来尝试获取锁。
悬挂指针/引用 避免让 detached 线程访问主线程中的局部变量。如果必须访问,使用智能指针 (std::shared_ptr) 来管理内存,或者使用线程安全的队列来传递数据。
异常安全 在线程函数中捕获异常,或者使用 RAII 技术,确保在异常情况下也能正确处理线程的生命周期。

std::thread 是一个强大的工具,但是使用它需要小心谨慎。理解线程的生命周期管理,避免常见的陷阱,才能写出健壮的多线程程序。希望今天的讲解对大家有所帮助! 记住,多线程编程的精髓在于“同步与互斥”,掌握了这两点,你就能驾驭 std::thread,让你的程序飞起来!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注