C++ `std::jthread` (C++20):自动 `join` 与协作取消机制

好的,没问题,直接进入主题!

大家好!今天我们要聊聊C++20中闪亮登场的std::jthread。这玩意儿可不是std::thread的简单升级版,而是一个更智能、更安全的多线程利器。它自带“自动join”和“协作取消”两大绝技,能让我们的多线程代码优雅又高效。

1. std::thread的那些糟心事儿

std::jthread横空出世之前,我们用std::thread创建线程,日子过得并不总是那么舒坦。比如,忘记joindetach线程,轻则导致程序异常退出,重则造成内存泄漏,简直是噩梦。

先看一个简单的例子:

#include <iostream>
#include <thread>

void do_something() {
  std::cout << "Thread is doing something...n";
  // 模拟耗时操作
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "Thread finished.n";
}

int main() {
  std::thread t(do_something);
  // Oops! 忘记join或detach了!
  return 0;
}

这段代码编译可以通过,但是运行的时候,如果 tmain 函数结束前还没有执行完,程序就会直接 terminate,因为 t 析构的时候会检查是否 joinable,如果 joinable 且没有 joindetach,就会调用 std::terminate

我们需要手动处理线程的生命周期,确保线程在程序结束前要么执行完毕(join),要么和主线程分离(detach)。但是人总有疏忽的时候,一旦忘记处理,就会埋下隐患。

2. std::jthread:自动join,妈妈再也不用担心我忘记join了!

std::jthread就像一个贴心的管家,它会在自身析构时自动join其管理的线程。这意味着,只要你创建了一个std::jthread对象,就再也不用担心忘记join导致程序崩溃了。

#include <iostream>
#include <thread>
#include <jthread> // 注意:需要包含这个头文件

void do_something() {
  std::cout << "Thread is doing something...n";
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "Thread finished.n";
}

int main() {
  std::jthread t(do_something);
  // 即使没有显式调用join,程序也不会崩溃,因为jthread会自动join
  return 0;
}

在这个例子中,我们把std::thread换成了std::jthread,其他代码保持不变。现在,即使我们忘记了join,程序也能正常退出,因为std::jthread的析构函数会自动调用join

3. std::jthread的构造函数

std::jthread的构造函数和std::thread类似,可以接受一个可调用对象作为参数。

  • 默认构造函数: 创建一个没有关联线程的std::jthread对象。
  • 带可调用对象的构造函数: 创建一个关联了线程的std::jthread对象。
  • 移动构造函数: 将一个std::jthread对象的所有权转移到另一个std::jthread对象。
  • 拷贝构造函数: std::jthread 禁止拷贝构造,避免资源管理混乱。
#include <iostream>
#include <jthread>

void my_function(int arg) {
  std::cout << "Thread executing with argument: " << arg << std::endl;
}

int main() {
  // 使用可调用对象构造jthread
  std::jthread t1(my_function, 42);

  // 使用 lambda 表达式构造 jthread
  std::jthread t2([](const std::stop_token& stoken) {
    while (!stoken.stop_requested()) {
      std::cout << "Thread running...n";
      std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "Thread stopped.n";
  });

  // 移动构造
  std::jthread t3 = std::move(t1); // t1不再拥有线程,t3拥有

  // t1 现在是空的,不再管理任何线程
  if (t1.joinable()) {
    std::cout << "t1 is joinablen"; // 这段代码不会执行
  } else {
    std::cout << "t1 is not joinablen"; // 这段代码会执行
  }

  std::this_thread::sleep_for(std::chrono::seconds(3));
  t2.request_stop(); // 请求 t2 停止

  return 0;
}

4. 协作取消:优雅地停止线程

std::jthread的另一个杀手锏是“协作取消”。它允许你向线程发送取消请求,线程可以选择在合适的时机响应这个请求,从而实现优雅的停止。

std::jthread向线程函数传递一个std::stop_token对象。线程函数可以通过stop_token.stop_requested()方法检查是否收到了取消请求。如果收到了请求,线程可以选择停止执行并退出。

#include <iostream>
#include <jthread>

void do_something(std::stop_token stoken) {
  while (!stoken.stop_requested()) {
    std::cout << "Thread is working...n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Thread is stopping...n";
}

int main() {
  std::jthread t(do_something);

  // 模拟主线程执行一段时间后,请求取消线程
  std::this_thread::sleep_for(std::chrono::seconds(3));
  t.request_stop(); // 发送取消请求

  return 0;
}

在这个例子中,线程函数do_something会不断循环,直到stop_token.stop_requested()返回true。在main函数中,我们等待3秒后,调用t.request_stop()发送取消请求。线程函数收到请求后,会停止循环并退出。

5. std::stop_tokenstd::stop_source

std::stop_token是只读的,线程函数只能通过它来检查是否收到了取消请求。如果你需要在多个线程之间共享取消信号,可以使用std::stop_source

std::stop_source是取消信号的来源,它可以创建std::stop_token对象,并向所有持有相同std::stop_token对象的线程发送取消请求。

#include <iostream>
#include <jthread>
#include <vector>

void do_something(std::stop_token stoken, int id) {
  while (!stoken.stop_requested()) {
    std::cout << "Thread " << id << " is working...n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Thread " << id << " is stopping...n";
}

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

  // 创建多个线程,并传递相同的stop_token
  for (int i = 0; i < 3; ++i) {
    threads.emplace_back(do_something, stop_source.get_token(), i);
  }

  // 模拟主线程执行一段时间后,请求取消所有线程
  std::this_thread::sleep_for(std::chrono::seconds(5));
  stop_source.request_stop(); // 发送取消请求给所有线程

  return 0;
}

在这个例子中,我们创建了一个std::stop_source对象,并使用stop_source.get_token()方法获取std::stop_token对象,传递给多个线程。当调用stop_source.request_stop()时,所有持有相同std::stop_token对象的线程都会收到取消请求。

6. std::stop_callback:取消时的回调

有时候,我们希望在线程收到取消请求时执行一些清理工作。std::stop_callback可以让我们注册一个回调函数,在线程收到取消请求时自动执行。

#include <iostream>
#include <jthread>

void do_something(std::stop_token stoken) {
  // 在收到取消请求时执行的回调函数
  std::stop_callback callback(stoken, []() {
    std::cout << "Cancellation callback executed.n";
    // 在这里执行清理工作
  });

  while (!stoken.stop_requested()) {
    std::cout << "Thread is working...n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Thread is stopping...n";
}

int main() {
  std::jthread t(do_something);

  // 模拟主线程执行一段时间后,请求取消线程
  std::this_thread::sleep_for(std::chrono::seconds(3));
  t.request_stop(); // 发送取消请求

  return 0;
}

在这个例子中,我们使用std::stop_callback注册了一个回调函数,当线程收到取消请求时,回调函数会被自动执行。

7. jthread vs thread:对比表格

特性 std::thread std::jthread
自动join 不支持 支持
协作取消 不支持 支持
异常安全性 较低 较高
默认行为 需要手动管理 自动管理
是否可拷贝 禁止 禁止
是否可移动 支持 支持

8. 何时使用std::jthread

  • 需要自动管理线程生命周期时: std::jthread可以避免忘记joindetach导致的错误。
  • 需要优雅地停止线程时: std::jthread的协作取消机制可以让你在线程收到取消请求时执行一些清理工作。
  • 希望代码更安全、更易维护时: std::jthread的自动管理和协作取消机制可以减少代码中的错误,提高代码的可读性和可维护性。

9. 注意事项

  • 如果线程函数抛出异常,std::jthread会自动捕获异常并重新抛出,这可以避免程序崩溃。
  • 如果线程函数长时间阻塞,std::jthread的自动join可能会导致主线程阻塞。在这种情况下,可以考虑使用std::future或其他异步机制。
  • request_stop()只是发送一个请求,线程是否真正停止取决于线程函数的实现。 线程函数必须检查 stop_token 并做出响应。

10. 总结

std::jthread是C++20中一个非常有用的多线程工具。它通过自动join和协作取消机制,简化了多线程编程,提高了代码的安全性、可读性和可维护性。在编写多线程代码时,应该优先考虑使用std::jthread

希望今天的讲解对大家有所帮助!记住,std::jthread是你的多线程好帮手,有了它,再也不用担心线程管理的那些糟心事儿啦!下次再见!

发表回复

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