C++ `std::jthread`:C++20 线程管理与自动 `join`

好的,各位观众,欢迎来到今天的C++线程管理脱口秀!今天咱们要聊的是C++20里面一位重量级选手——std::jthread。这玩意儿啊,解决了C++多线程编程里一个老大难的问题:线程忘记join导致的程序崩溃。

开场白:线程的爱恨情仇

在C++11引入std::thread之后,多线程编程的大门算是彻底打开了。但是,开门容易进门难,进门之后发现坑更多。最常见的一个坑就是线程对象的生命周期管理。如果你创建了一个std::thread对象,并且线程还在跑,而std::thread对象就被销毁了,那程序就等着崩溃吧!

为啥呢?因为std::thread的析构函数会检查线程是否还在joinable状态。如果还在,它就会调用std::terminate()直接结束程序。这就像你租了个房子,结果房东在你还没搬走的时候就把房子给拆了,你不得跟房东拼命?

所以,使用std::thread的时候,你必须手动调用join()或者detach()来处理线程的生命周期。join()就是等着线程跑完,detach()就是让线程自己跑,跟主线程脱离关系。但是,程序员也是人,是人就会犯错。万一忘了join()或者detach(),那就是一场灾难。

std::jthread:拯救世界的英雄

C++20为了拯救我们这些手残党,推出了std::jthreadj代表啥?joining!也就是说,std::jthread在析构的时候会自动join()它管理的线程。这就像一个负责任的房东,在你搬走之前绝不会拆房子,而是会耐心等你处理完所有事情。

std::jthread的基本用法

std::jthread的用法和std::thread差不多,但是更安全、更方便。咱们先来看一个简单的例子:

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

void worker_thread(int id) {
  std::cout << "线程 " << id << " 正在运行...n";
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "线程 " << id << " 运行结束。n";
}

int main() {
  std::jthread t1(worker_thread, 1);
  std::jthread t2(worker_thread, 2);

  std::cout << "主线程正在运行...n";
  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::cout << "主线程运行结束。n";

  return 0;
}

在这个例子中,我们创建了两个std::jthread对象t1t2,分别执行worker_thread函数。主线程也睡了一秒钟。当main函数结束的时候,t1t2会被销毁,它们的析构函数会自动调用join(),等待线程执行完毕。这样,我们就不用担心线程忘记join()的问题了。

std::jthread的优势

  • 自动join() 这是std::jthread最大的优势,也是它最吸引人的地方。妈妈再也不用担心我忘记join()了!
  • 支持中断: std::jthread可以被中断。这意味着你可以优雅地停止一个正在运行的线程,而不是粗暴地结束它。
  • 传递std::stop_token std::jthread的构造函数可以接受一个std::stop_token参数。这个stop_token可以用来检查线程是否被请求停止。

std::stop_token:优雅地停止线程

std::stop_token是C++20引入的一个新特性,用于请求线程停止执行。std::jthread可以接收一个std::stop_token,并在线程函数中使用它来检查是否需要停止。

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

void worker_thread_with_stop_token(std::stop_token stop_token, int id) {
  std::cout << "线程 " << id << " 正在运行...n";
  while (!stop_token.stop_requested()) {
    std::cout << "线程 " << id << " 正在工作中...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
  }
  std::cout << "线程 " << id << " 收到停止请求,即将退出。n";
}

int main() {
  std::jthread t(worker_thread_with_stop_token, 1);

  std::cout << "主线程正在运行...n";
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "主线程请求停止线程。n";
  t.request_stop();
  std::cout << "主线程运行结束。n";

  return 0;
}

在这个例子中,worker_thread_with_stop_token函数接收一个std::stop_token参数。它使用stop_token.stop_requested()来检查是否被请求停止。主线程在运行2秒后,调用t.request_stop()来请求停止线程。线程收到停止请求后,会优雅地退出。

std::jthread的构造函数

std::jthread提供了多个构造函数,可以满足不同的需求:

构造函数 描述
std::jthread() 创建一个空的std::jthread对象,不关联任何线程。
std::jthread(Fn&& fn, Args&&... args) 创建一个std::jthread对象,并启动一个线程执行fn(std::forward<Args>(args)...)
std::jthread(std::stop_token stoken, Fn&& fn, Args&&... args) 创建一个std::jthread对象,并启动一个线程执行fn(stoken, std::forward<Args>(args)...)。线程函数可以接收一个std::stop_token参数,用于检查是否被请求停止。
std::jthread(const std::jthread& other) 拷贝构造函数,但是被删除。std::jthread对象不能被拷贝。
std::jthread(std::jthread&& other) noexcept 移动构造函数,将other对象管理的线程的所有权转移给新的std::jthread对象。other对象不再管理任何线程。

std::jthread的成员函数

成员函数 描述
join() 等待线程执行完毕。如果线程已经执行完毕,则立即返回。
detach() 将线程与std::jthread对象分离。线程将继续独立运行,不再受std::jthread对象管理。
get_id() 返回线程的ID。
native_handle() 返回底层线程的句柄。
hardware_concurrency() 返回硬件并发线程数。这个函数是一个静态成员函数。
joinable() 检查线程是否是可joinable的。如果线程已经执行完毕或者已经被detach,则返回false,否则返回true
request_stop() 请求停止线程。这会设置std::stop_token的停止标志。
get_stop_token() 返回与std::jthread对象关联的std::stop_token对象。
swap(std::jthread& other) 交换两个std::jthread对象所管理的线程的所有权。

std::jthreadstd::thread的比较

特性 std::thread std::jthread
自动join()
支持中断
构造函数 多个 多个
移动构造 支持 支持
拷贝构造 支持(C++11),但通常不建议 删除

std::jthread的注意事项

  • 异常处理: 即使std::jthread会自动join(),你也需要注意线程函数中的异常处理。如果线程函数抛出异常,并且没有被捕获,那么程序仍然会崩溃。
  • 死锁: 使用std::jthread并不能完全避免死锁的发生。如果多个线程互相等待对方释放资源,那么仍然会发生死锁。
  • detach() 虽然std::jthread会自动join(),但是你仍然可以调用detach()来分离线程。但是,一旦你调用了detach()std::jthread对象就不再管理线程的生命周期了,你需要自己负责确保线程在程序结束前执行完毕。

一个更复杂的例子:生产者-消费者模型

咱们来一个更复杂的例子,使用std::jthreadstd::stop_token来实现一个简单的生产者-消费者模型。

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

// 共享数据
std::queue<int> queue;
std::mutex mutex;
std::condition_variable cv;

// 生产者线程
void producer(std::stop_token stop_token) {
  int i = 0;
  while (!stop_token.stop_requested()) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::unique_lock<std::mutex> lock(mutex);
    queue.push(i++);
    std::cout << "生产者生产了: " << i - 1 << std::endl;
    lock.unlock();
    cv.notify_one(); // 通知消费者
  }
  std::cout << "生产者收到停止请求,即将退出。n";
}

// 消费者线程
void consumer(std::stop_token stop_token) {
  while (!stop_token.stop_requested()) {
    std::unique_lock<std::mutex> lock(mutex);
    cv.wait(lock, [&]() { return !queue.empty() || stop_token.stop_requested(); }); // 等待队列不为空或者收到停止请求
    if (stop_token.stop_requested() && queue.empty()) {
        std::cout << "消费者收到停止请求,队列为空,即将退出。n";
        return;
    }
    int data = queue.front();
    queue.pop();
    std::cout << "消费者消费了: " << data << std::endl;
    lock.unlock();
  }
  std::cout << "消费者收到停止请求,即将退出。n";
}

int main() {
  std::jthread producer_thread(producer);
  std::jthread consumer_thread(consumer);

  std::cout << "主线程正在运行...n";
  std::this_thread::sleep_for(std::chrono::seconds(5));
  std::cout << "主线程请求停止生产者和消费者。n";
  producer_thread.request_stop();
  consumer_thread.request_stop();
  std::cout << "主线程运行结束。n";

  return 0;
}

在这个例子中,生产者线程不断地向队列中添加数据,消费者线程不断地从队列中取出数据。主线程在运行5秒后,会请求停止生产者和消费者线程。生产者和消费者线程都会检查std::stop_token的停止标志,并在收到停止请求后优雅地退出。

总结:std::jthread,多线程编程的得力助手

std::jthread是C++20中一个非常实用的特性,它可以帮助我们更安全、更方便地进行多线程编程。它自动join()的特性可以避免线程忘记join()导致的程序崩溃,std::stop_token可以让我们优雅地停止线程。如果你正在使用C++20,那么std::jthread绝对值得你尝试。

记住,std::jthread虽然强大,但也不是万能的。你仍然需要注意线程函数中的异常处理,避免死锁的发生。只有掌握了多线程编程的基本知识,才能真正发挥std::jthread的威力。

好了,今天的脱口秀就到这里。感谢大家的收看,咱们下期再见!希望大家以后写多线程程序,都能像用std::jthread一样轻松愉快!

发表回复

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