新的线程:C++20 std::jthread

2023-11-05 17:36:09 浏览数 (1)

1. std::jthread是什么

jthread表示单个执行线程。它拥有通常同 std::thread 的行为,除了jthread在析构时自动再结合,而且能在具体情况下取消/停止。

2. 为什么要引入jthread

std::jthread std::thread 基础上,增加了能够主动取消或停止线程执行的新特性。与 std::thread 相比,std::jthread 具有异常安全的线程终止流程,并且在大多数情况下可以替换它,只需很少或无需更改代码。在我们进入细节之前,先说一说std::thread 的缺陷:std::jthread 使用的时候需要通过join()来完成等待线程结束,继续join()后语句的执行,或者调用detach()来让线程与当前线程分离,移至后台继续运行。

A std::thread instance can be in either the joinable or unjoinable state. A std::thread that is default constructed, detached, or moved is unjoinable. We must join a joinable std::thread explicitly before the end of its life; otherwise, the std::thread's destructor calls std::terminate, whose default behavior is to abort the process. std::thread 实例可以处于可联接或不可联接状态。默认构造、分离或移动的 std::thread 不可联接。我们必须在可连接的 std::thread 生命周期结束之前显式加入它;否则,std::thread 的析构函数将调用 std::terminate,其默认行为是中止进程。

代码语言:javascript复制
void FuncWithoutJoinOrDetach() {
  std::thread t{task, task_args};
  // 没有调用t.join()或t.detach()
}  // t的生命周期结束时将调用std::terminate(),异常结束程序

以上述代码所示,如果没有调用t.join()t.detach(),当线程对象t生命周期结束的时候,可能会产生core dump,导致程序异常终止。上述例子中,在实例化对象t后,即使调用线程tjoin()函数,有时候可能需要等待很长时间才能将线程ttask执行完成,甚至是永久的等待(例如task中存在死循环),由于thread不像进程一样允许我们主动将其kill掉,所以当t中出现死循环,会导致无法继续执行jion()之后的语句,已经启动的线程只能自己结束运行或结束整个程序来结束该线程。基于以上两个主要原因,在C 20中引入std::jthread类,来弥补std::tread的缺陷,其除了拥有std::thread 的行为外主要新增了以下两个功能:

  • std::jthread 对象被析构时,会自动调用join,等待其所表示的执行流结束。
  • std::jthread支持外部请求中止。

3. 如何使用

std::jthred的基础使用方法与std::thread的用法一样,这里我们不再赘述,下面我们通过几个例子重点介它新增的两个功能。

3.1自动join()

我们先看一个std::thread的错误例子:

代码语言:javascript复制
#include <iostream>
#include <thread>

int main() {
    
    std::cout << 'n';
    std::cout << std::boolalpha;
    
    std::thread thr{[]{ std::cout << "Joinable std::thread" << 'n'; }};
    
    std::cout << "thr.joinable(): " << thr.joinable() << 'n';
    std::cout << 'n';
    return 0;
}

可能的输出:

代码语言:javascript复制
thr.joinable(): true

libc  abi: terminating
[1]    2326 abort      ./test

从输出可以看出程序异常终止了。

下面我们将thread替换为jthread,由于jthread的对象thr在析构的时候,会自动调用自身的join函数,保证主线程要等待thr执行完毕再进行下一步操作。

代码语言:javascript复制
#include <iostream>
#include <thread>

int main()
{

  std::cout << 'n';
  std::cout << std::boolalpha;

  std::jthread thr{[]
                   { std::cout << "Joinable std::thread" << 'n'; }};

  std::cout << "thr.joinable(): " << thr.joinable() << 'n';

  std::cout << 'n';
  return 0;
}

输出:

代码语言:javascript复制

thr.joinable(): true

Joinable std::thread

3.2 线程中断

对于线程中断,std::jthread主要引入以下三个停止信号处理:

  • get_stop_source() :返回与线程的停止状态关联的 stop_source 对象。
  • get_stop_token() :返回与线程的共享停止状态关联的 stop_token
  • **request_stop() **:请求执行经由线程的共享停止状态停止。

下面我们通过几个个例子简单了解一下它的具体用法。

示例1:

代码语言:javascript复制
#include <chrono>
#include <iostream>
#include <thread>

using namespace::std::literals;

int main() {
    
    std::cout << 'n';
    
    std::jthread nonInterruptable([]{                           // 步骤(1)
        int counter{0};
        while (counter < 10){
            std::this_thread::sleep_for(0.2s);
            std::cerr << "nonInterruptable: " << counter << 'n'; 
              counter;
        }
    });
    
    std::jthread interruptable([](std::stop_token stoken){     // 步骤(2)
        int counter{0};
        while (counter < 10){
            std::this_thread::sleep_for(0.2s);
            if (stoken.stop_requested()) return;               // 步骤(3)
            std::cerr << "interruptable: " << counter << 'n'; 
              counter;
        }
    });
    
    std::this_thread::sleep_for(1s);
    
    std::cerr << 'n';
    std::cerr << "Main thread interrupts both jthreads" << 'n';
    nonInterruptable.request_stop();                           // 步骤(4)
    interruptable.request_stop();                              // 步骤(5)
    
    std::cout << 'n';
    
}

输出:

代码语言:javascript复制
nonInterruptable: interruptable: 00

nonInterruptable: 1
interruptable: 1
nonInterruptable: 2
interruptable: 2
nonInterruptable: 3
interruptable: 3

Main thread interrupts both jthreads


nonInterruptable: 4
nonInterruptable: 5
nonInterruptable: 6
nonInterruptable: 7
nonInterruptable: 8
nonInterruptable: 9

在示例1中,我们启动两个线程:nonInterruptable(步骤1)和nterruptable(步骤2)。与 nonInterruptable 不同的是 interruptable 获取一个 std::stop_token 并在步骤3中使用它来检查它是否被中断(即stoken.stop_requested() )。如果发生停止请求,lambda 函数将返回,interruptable线程将结束。主程序在步骤5的地方调用interruptable.request_stop();,触发停止请求,此时interruptable线程停止,不再进行打印。虽然在步骤4调用了nonInterruptable.request_stop(); ,但在nonInterruptable里未进行对该请求的处理,因此nonInterruptable继续执行,直到主程序结束。

示例2:

代码语言:javascript复制
#include <chrono>
#include <iostream>
#include <thread>
 
using namespace std::chrono_literals;
 
int main() {
    std::cout << std::boolalpha;
    auto print = [](std::string_view name, const std::stop_source &source) {
        std::cout << name << ": stop_possible = " << source.stop_possible();
        std::cout << ", stop_requested = " << source.stop_requested() << 'n';
    };
 
    // A worker thread
    auto worker = std::jthread([](std::stop_token stoken) {
        for (int i = 10; i; --i) {
            std::this_thread::sleep_for(300ms);
            if (stoken.stop_requested()) {
                std::cout << "  Sleepy worker is requested to stopn";
                return;
            }
            std::cout << "  Sleepy worker goes back to sleepn";
        }
    });
 
    std::stop_source stop_source = worker.get_stop_source();
    print("stop_source", stop_source);
 
    std::cout << "nPass source to other thread:n";
    auto stopper = std::thread(
        [](std::stop_source source) {
            std::this_thread::sleep_for(500ms);
            std::cout << "Request stop for worker via sourcen";
            source.request_stop();
        },
        stop_source);
    stopper.join();
    std::this_thread::sleep_for(200ms);
    std::cout << 'n';
 
    print("stop_source", stop_source);
}

输出:

代码语言:javascript复制
stop_source: stop_possible = true, stop_requested = false

Pass source to other thread:
  Sleepy worker goes back to sleep
Request stop for worker via source
  Sleepy worker is requested to stop

stop_source: stop_possible = true, stop_requested = true

示例3:

代码语言:javascript复制
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

int main() {
    auto f = [](const stop_token &st) { // jthread负责传入stop_token
        while (!st.stop_requested()) { // jthread并不会强制停止线程,需要我们依据stop_token的状态来进行取消/停止操作
            cout << "other: " << this_thread::get_id() << "n";
            this_thread::sleep_for(1s);
        }
        cout << "other thread stopped!n";
    };
    jthread jth(f);

    cout << "main: " << this_thread::get_id() << "n";
    this_thread::sleep_for(5s);

    jth.request_stop(); // 请求停止线程,对应的stop_token的stop_requested()函数返回true(注意,除了手动调用外,jthread销毁时也会自动调用该函数)
    // 我们无需在jthread上调用join(),它在销毁时自动join
}
}

可能的输出:

代码语言:javascript复制
main: other: 140166751352704
140166751336192
other: 140166751336192
other: 140166751336192
other: 140166751336192
other: 140166751336192
other thread stopped!

4. 总结

jthread基于std::thread主要增加了以下两个功能:

  • jthread 对象被析构时,会自动调用join,等待其所表示的执行流结束。
  • jthread支持外部请求中止(通过 get_stop_sourceget_stop_token request_stop )。

std::jthread 中的自动join和外部请求中止功能使编写更安全的代码变得更加容易,但其性能上相对于thread也增加了开销。

0 人点赞