C++ `std::jthread` (C++20):线程管理与协作取消的简化

哈喽,各位好!今天咱们聊聊C++20里的一个宝贝疙瘩——std::jthread。这玩意儿可不是你奶奶用的缝纫机(虽然名字有点像),而是C++多线程编程里的一颗新星,专门解决线程管理和协作取消的问题。

一、线程,永恒的难题

在进入std::jthread的世界之前,咱们先回顾一下线程这玩意儿为啥让人头疼。多线程编程就像同时指挥一支乐队,每个乐器(线程)都有自己的节奏,搞不好就乱成一锅粥。

最常见的麻烦包括:

  • 资源竞争: 多个线程争抢同一份资源,比如一块内存,一个文件,结果谁也用不好。
  • 死锁: 线程A等着线程B释放资源,线程B又等着线程A释放资源,大家互相等着,谁也动不了,程序就僵住了。
  • 线程生命周期管理: 线程啥时候开始,啥时候结束?结束之后资源怎么释放?这些都得操心,一不小心就内存泄漏。
  • 线程同步: 线程之间需要协调工作,比如生产者线程生产数据,消费者线程消费数据,得保证数据不丢,也不重复消费。

C++11引入了std::thread,算是给多线程编程开了个头,但它只负责创建和启动线程,其他的事情还得你自己来。这意味着你要手动join或者detach线程,手动管理线程的生命周期,手动处理异常,手动进行同步。

std::thread就像一个原始的手动挡汽车,开起来很刺激,但是也很累。而std::jthread就像一个自动挡汽车,它帮你处理了很多琐碎的事情,让你更专注于业务逻辑。

二、std::jthread:自动挡的线程管理

std::jthread是对std::thread的改进,它主要解决了两个问题:

  1. 自动join: std::jthread对象销毁时,会自动join它管理的线程,避免线程detached导致的资源泄漏。
  2. 协作取消: std::jthread提供了一种优雅的方式来取消线程的执行,避免线程强制终止带来的问题。

咱们先来看一个简单的例子:

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

void worker_function() {
  std::cout << "Worker thread startedn";
  std::this_thread::sleep_for(std::chrono::seconds(3));
  std::cout << "Worker thread finishedn";
}

int main() {
  std::jthread my_thread(worker_function); // 创建并启动线程

  std::cout << "Main thread continues...n";
  std::this_thread::sleep_for(std::chrono::seconds(1));

  // my_thread对象销毁时,会自动join它管理的线程
  return 0;
}

在这个例子中,std::jthread my_thread(worker_function);创建并启动了一个线程,执行worker_function。当main函数结束时,my_thread对象会被销毁,它的析构函数会自动调用join(),等待worker_function执行完毕。

如果你用std::thread来实现同样的功能,你需要手动调用my_thread.join(),否则程序可能会崩溃或者出现未定义的行为。

三、std::jthread的协作取消

std::jthread提供的协作取消机制,比直接杀死线程要优雅得多。它允许线程自己决定何时停止执行,从而避免数据损坏或者资源泄漏。

要使用协作取消,你需要:

  1. 获取std::stop_token std::jthread构造函数会传递一个std::stop_token给线程函数。
  2. 定期检查std::stop_token 线程函数需要定期检查std::stop_tokenstop_requested()方法,如果返回true,就表示线程应该停止执行了。

下面是一个例子:

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

void worker_function(std::stop_token stop_token) {
  std::cout << "Worker thread startedn";
  while (!stop_token.stop_requested()) {
    std::cout << "Worker thread is working...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
  }
  std::cout << "Worker thread stoppedn";
}

int main() {
  std::jthread my_thread(worker_function); // 创建并启动线程

  std::cout << "Main thread continues...n";
  std::this_thread::sleep_for(std::chrono::seconds(2));

  my_thread.request_stop(); // 请求停止线程

  std::cout << "Main thread finishedn";
  return 0;
}

在这个例子中,worker_function接收一个std::stop_token作为参数。在while循环中,它定期检查stop_token.stop_requested()的返回值。如果main函数调用了my_thread.request_stop()stop_token.stop_requested()就会返回trueworker_function就会跳出循环,停止执行。

四、std::jthread的构造函数

std::jthread的构造函数有很多重载版本,可以接受不同的参数:

  • 默认构造函数: 创建一个空的std::jthread对象,不启动任何线程。

    std::jthread my_thread; // 创建一个空的jthread对象
  • 可调用对象构造函数: 创建并启动一个线程,执行指定的可调用对象(函数、函数对象、lambda表达式等)。

    void my_function(int arg) {
      std::cout << "Thread function called with arg: " << arg << std::endl;
    }
    
    int main() {
      std::jthread my_thread(my_function, 10); // 创建并启动线程,传递参数10
      return 0;
    }
  • 移动构造函数: 将一个std::jthread对象的所有权转移到另一个std::jthread对象。

    std::jthread create_thread() {
      return std::jthread([]{
        std::cout << "Thread created and owned by the functionn";
        std::this_thread::sleep_for(std::chrono::seconds(1));
      });
    }
    
    int main() {
      std::jthread my_thread = create_thread(); // 创建线程,所有权转移给my_thread
      return 0;
    }

五、std::jthread的成员函数

std::jthread提供了一些有用的成员函数,用于管理线程:

  • join() 等待线程执行完毕。如果线程已经执行完毕或者没有启动,则立即返回。

    std::jthread my_thread([]{
      std::cout << "Thread is running...n";
      std::this_thread::sleep_for(std::chrono::seconds(2));
      std::cout << "Thread finishedn";
    });
    
    // ... 一些其他的代码 ...
    
    // 在需要的时候手动join线程
    //my_thread.join(); // 即使没有这行代码,jthread析构时也会自动join
  • detach() 将线程从std::jthread对象中分离出来。分离之后,线程会继续在后台运行,std::jthread对象不再管理它的生命周期。

    • 注意:强烈不推荐使用detach,除非你非常清楚自己在做什么。 分离的线程可能会导致资源泄漏或者未定义的行为。
    std::jthread my_thread([]{
      std::cout << "Detached thread is running...n";
      std::this_thread::sleep_for(std::chrono::seconds(2));
      std::cout << "Detached thread finishedn";
    });
    
    my_thread.detach(); // 将线程分离
  • request_stop() 请求停止线程的执行。

    std::jthread my_thread([](std::stop_token stoken){
      while(!stoken.stop_requested()){
        std::cout << "Running...n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
      }
      std::cout << "Stopping...n";
    });
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    my_thread.request_stop(); // 请求停止线程
  • get_stop_token() 获取与线程关联的std::stop_token对象。

    std::jthread my_thread([](std::stop_token stoken){
      while(!stoken.stop_requested()){
        std::cout << "Running...n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
      }
      std::cout << "Stopping...n";
    });
    
    std::stop_token token = my_thread.get_stop_token();
    if(token.stop_possible()){
        std::cout << "Stop is possible!n";
    }
  • joinable() 检查std::jthread对象是否管理一个活动的线程。

    std::jthread my_thread([]{
      std::cout << "Thread is running...n";
      std::this_thread::sleep_for(std::chrono::seconds(2));
      std::cout << "Thread finishedn";
    });
    
    if (my_thread.joinable()) {
      std::cout << "Thread is joinablen";
      //my_thread.join(); // jthread析构时会自动join
    }
  • swap() 交换两个std::jthread对象的状态。

    std::jthread thread1([]{
      std::cout << "Thread 1 runningn";
      std::this_thread::sleep_for(std::chrono::seconds(1));
    });
    
    std::jthread thread2([]{
      std::cout << "Thread 2 runningn";
      std::this_thread::sleep_for(std::chrono::seconds(1));
    });
    
    std::swap(thread1, thread2); // 交换两个线程的所有权
  • operator= 赋值运算符,可以将一个std::jthread对象赋值给另一个std::jthread对象。赋值会停止目标对象当前关联的线程(如果存在),然后将源对象的所有权转移到目标对象。

    std::jthread thread1([]{
      std::cout << "Thread 1 runningn";
      std::this_thread::sleep_for(std::chrono::seconds(1));
    });
    
    std::jthread thread2;
    thread2 = std::move(thread1); //thread1不再拥有线程的所有权

六、std::stop_sourcestd::stop_callback

除了 std::stop_token 之外,C++20 还提供了 std::stop_sourcestd::stop_callback,它们一起构成了一个完整的线程取消机制。

  • std::stop_source 负责发起停止请求。它可以创建多个 std::stop_token 对象,并将它们分发给不同的线程。
  • std::stop_callback 允许你在线程停止时执行一些清理工作。

下面是一个例子:

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

std::mutex mtx;

void cleanup_function() {
  std::lock_guard<std::mutex> lock(mtx);
  std::cout << "Cleanup function calledn";
}

void worker_function(std::stop_token stop_token) {
  std::stop_callback cleanup(stop_token, cleanup_function); // 注册清理函数
  std::cout << "Worker thread startedn";
  while (!stop_token.stop_requested()) {
    std::cout << "Worker thread is working...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
  }
  std::cout << "Worker thread stoppedn";
}

int main() {
  std::stop_source stop_source;
  std::jthread my_thread(worker_function, stop_source.get_token()); // 创建并启动线程

  std::cout << "Main thread continues...n";
  std::this_thread::sleep_for(std::chrono::seconds(2));

  stop_source.request_stop(); // 请求停止线程

  std::cout << "Main thread finishedn";
  return 0;
}

在这个例子中,std::stop_source stop_source; 创建了一个 std::stop_source 对象。stop_source.get_token() 获取一个与 stop_source 关联的 std::stop_token 对象,并将其传递给 worker_function

std::stop_callback cleanup(stop_token, cleanup_function); 注册了一个清理函数 cleanup_function,当线程停止时,该函数会被自动调用。

七、std::jthread的优点和缺点

优点:

  • 自动join: 避免线程detached导致的资源泄漏。
  • 协作取消: 提供了一种优雅的方式来取消线程的执行,避免线程强制终止带来的问题。
  • 简化线程管理: 减少了手动管理线程生命周期的代码。
  • 更安全: 降低了多线程编程出错的风险。

缺点:

  • C++20特性: 需要使用支持C++20的编译器。
  • 协作取消依赖于线程函数: 线程函数需要配合检查std::stop_token,才能实现协作取消。如果线程函数没有检查std::stop_tokenrequest_stop()就没有任何效果。
  • 性能开销: 协作取消机制可能会带来一定的性能开销,因为线程需要定期检查std::stop_token

八、std::jthread的应用场景

std::jthread适用于各种需要多线程编程的场景,特别是那些需要可靠的线程管理和协作取消的场景。

  • 后台任务处理: 可以使用std::jthread来执行后台任务,比如文件上传、数据处理、网络请求等。
  • 并发计算: 可以使用std::jthread将计算任务分解成多个子任务,并发执行,提高计算效率。
  • GUI程序: 可以使用std::jthread来执行耗时的GUI操作,避免阻塞主线程,提高程序的响应速度。
  • 游戏开发: 可以使用std::jthread来处理游戏中的各种任务,比如AI计算、物理模拟、渲染等。

九、总结

std::jthread是C++20中一个非常有用的线程管理工具,它简化了多线程编程,提高了程序的可靠性和安全性。虽然它有一些缺点,但是它的优点远远超过了缺点。如果你正在使用C++进行多线程编程,那么std::jthread绝对值得你学习和使用。

记住,多线程编程是一门复杂的艺术,需要深入理解线程的原理和各种同步机制。std::jthread只是一个工具,它可以帮助你更好地管理线程,但是它不能代替你对多线程编程的理解。

希望今天的讲解对大家有所帮助!下次再见!

发表回复

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