C++20中的jthread

一、多线程开发

c++11以前,是不包含线程库的,开发多线程,必须使用OS系统自带的线程API,这就导致了很多问题,最主要的是,跨平台的开发,一般要保持几个主流应用的库的代码支持,特别是对Win平台和Linux平台的两个线程库的支持,否则,就无法实现简单的跨平台的功能。而两个平台的多线程的使用差异和一些细节不同的,比如事件Event在Linux上没有。而条件变量在Win平台上没有,就更容易使一些应用场景的开发的复杂性大幅提高。

而到了c++11以后,提供了std::thread这个多线程的支持,可以说大幅的降低了跨平台开发的复杂性。

但是在实际的应用过程中,又遇到了一些新的需求变化,比如应用的方便性和多线程运行过程中的可操作性及可见性上,都需要提供新的接口。按照c++设计者们的一贯作风,他们不会在基础的std::thread本身进行修改,那么,他们就对这个类进行了再封装,增加了上述的功能,然后提出了std::jthread这个类。

二、jthread的功能和实现

一般来说,接口越丰富,那么这个类功能越强大,但是反过来,缺点就是不容易掌握并且容易忘记调用,在使用std::thread时,有没有忘记调用join()或者detach()的时候?自动调用好不好?线程函数运行过程中,可不可以可以动态请求交互一下,而不是靠各种变量来预先进行控制。对,在std::jthread上都实现了。

看一看标准文档是如何定义的:

c 复制代码
std::jthread::jthread
jthread() noexcept;                              (1)	(C++20 起)
jthread( jthread&& other ) noexcept;             (2)	(C++20 起)
template< class Function, class... Args >
explicit jthread( Function&& f, Args&&... args );(3)	(C++20 起)
jthread( const jthread& ) = delete;              (4)	(C++20 起)

第一个表示创建新的jthread对象;第二个表示移动语义,既然移动了,那么原来线程停止执行线程,由新线程执行;第三个表示线程函数f接受 std::stop_token 作为其首参数,则新线程开始执行:

c 复制代码
std::invoke(decay_copy(std::forward<Function>(f)),
            get_stop_token(),
            decay_copy(std::forward<Args>(args))...);

;否则它开始执行

c 复制代码
std::invoke(decay_copy(std::forward<Function>(f)),
            decay_copy(std::forward<Args>(args))...);

任一情况下, decay_copy 定义为

c 复制代码
template <class T>
std::decay_t<T> decay_copy(T&& v) { return std::forward<T>(v); }

除了在调用方的语境中执行 decay_copy ,故而在求值和移动/构造参数期间抛出的任何异常都在当前线程抛出,而不会开始新线程。

构造函数的完成同步于(按 std::memory_order 中定义) f 的副本在新线程上调用的开始。

若 std::remove_cvref_t 与 std::jthread 为相同类型则此构造函数不参与重载决议。

最后一个表示不可复制,也就是说只能它自己执行线程,没有任何其它一个std::jthread对象可以表示同一线程。

std::jthread的成员函数里,可以看支持查看硬件并发数的函数,交换线程对象的函数,协作请示的函数等,更多的详情可以参看一下:

https://zh.cppreference.com/w/cpp/thread/jthread/jthread

https://zh.cppreference.com/w/cpp/header/stop_token

三、应用

线程的中断和取消是需要高度引起注意的,一不小心,强行KILL的线程中的各种资源或者句柄就没有回收,导致各种泄露。这也是在多线程编程中,普遍有一个要求,就是让线程自己执行完毕退出,而为了这个要求,就需要牺牲一下控制的效率。

当然,先从std::thread的多线程编程的例子看:

c 复制代码
#include <Windows.h>
#include <iostream>
#include <thread>
#include <chrono>

bool quit = false;

void SleepTime(int sec)
{
    std::this_thread::sleep_for(std::chrono::seconds(sec));
}
void ThreadWorker()
{
    //work work work
    std::cout << "thread start,working! " << std::endl;
}

void MainWorker()
{
    std::cout << "main thread start,working! " << std::endl;
}
int main()
{
    std::thread t = std::thread([&]() {
        while (!quit)
        {
            ThreadWorker();
            SleepTime(2);
        }
    });

    //Sleep(3);
    MainWorker();
    t.join();//t.detach();
}

这个DEMO非常简单,在主线程中启动了一个子线程,需要注意一下注释的延时的情况,主要是起一个先后交替的条件。主线程在执行完成自己的工作后,就会join等待子线程的完成,像上面这种情况,就死循环了,不会停止。如果需要停止只能使用设置quit为True或者强制的命令等不友好的手段了。首先看一下,换成jthread会是啥样?其它代码都没有改变,只是修改了下面的代码:

c 复制代码
std::jthread t = std::jthread([&]() {
    while (!quit)
    {
        ThreadWorker();
        SleepTime(2);
    }
});

需要包含的jthread等的头文件可以去github或者相关的网站上下载,这里只提供一处,并对此表示感谢:

https://github.com/josuttis/jthread/tree/master/source

上面的两处代码的运行结果是完全一样的,没有什么不同。那用这个类的优势呢?别急,把main函数改一下:

c 复制代码
int main()
{
    std::jthread t = std::jthread([&]() {
        while (!quit)
        {
            ThreadWorker();
            SleepTime(2);
        }
    });

    Sleep(3);
    MainWorker();
    //t.join();//t.detach();
}

把最后一行注释,再编译运行,仍然没有啥问题,有一点不同了吧。这可以避免粗心大意的同学出现意外错误吧。再看一下,如果设置quit为true,那么线程会怎么办?没啥大问题,到了判断就退出了,可是如果有多个线程在执行,是不是要设置多个类似的变量呢?答案是肯定的。这时候儿看看jthread的协作中断:

c 复制代码
int main()
{
    std::jthread t = std::jthread([&]() {
        while (!quit)
        {
            ThreadWorker();
            SleepTime(2);
        }
    });

    Sleep(3);
    MainWorker();
    t.request_stop();//简单增加一行代码
}

编译运行,发现没啥反应,好,再修改一下线程函数:

c 复制代码
int main()
{
    std::jthread t = std::jthread([&](std::stop_token st) {
        while (/*!quit*/!st.stop_requested())
        {
            ThreadWorker();
            SleepTime(2);
        }
    });

    Sleep(3);
    MainWorker();
    //getchar();
    t.request_stop();
    //t.join();//t.detach();
    getchar();
}

这个时候儿发现没啥问题了,注意上面getchar()函数的位置,如果在后面,则执行一次就退出了。如果在前面,就继续不断执行。把t.request_stop();这一行也注释,发现程序仍然和不注释一样,看来这个jthread里做了什么运作,看一下它的析构函数:

c 复制代码
inline jthread::~jthread() {
  if (joinable()) {   // if not joined/detached, signal stop and wait for end:
    request_stop();
    join();
  }
}

明白了吧。至于为什么不用detach(),可能是大牛们觉得join()会更安全一些吧。毕竟等待完成后,所有的对象都被回收了。下面再看官网提供的一个例子:

c 复制代码
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
int main()
{
    foo f;
    std::jthread t3(&foo::bar, &f);
    std::jthread t = std::jthread([&](std::stop_token st) {
        while (/*!quit*/!st.stop_requested())
        {
            ThreadWorker();
            SleepTime(2);
        }
    });
    std::cout << t.hardware_concurrency() << std::endl;

    Sleep(3);
    MainWorker();
    getchar();
    t.request_stop();
    //t.join();//t.detach();
}

执行一下,就可以看出上面提到的decay_copy这个模板函数的使用,没啥不容易理解的。

4、总结

其实还有几个特点值得说说,但不准备再讲了,虽然已经投票完成确定了c++20的标准,但毕竟还没有正式增加进去,大家有兴趣可以看看源码。整体来看,c++应用还是越来越方便,这对c++来说,肯定是个好事情。现在就希望>VS2019或者>GCC11中,尽快完全支持。

相关推荐
sun00770014 小时前
MISRA C++ 2023 编码标准&规范
c++20
飞翔的薄荷4 天前
C++20 时间转本地时间,时间转字符串以及字符串转时间的方法
算法·c++20
何曾参静谧10 天前
「C/C++」C++20 之 #include<ranges> 范围
c语言·c++·c++20
年轻的古尔丹11 天前
【C++ 20进阶(1):模块导入 import】
c++20·c++ module·c++ import·c++ export·c++ 模块
fengqiao199917 天前
C++ 20 Concept
c++20
好看资源平台17 天前
C++卓越:全面提升专业技能的深度课程(第一章第一课C++17与C++20概述)
c++·算法·c++20
charlie11451419117 天前
C++20 STL CookBook读书笔记1
开发语言·c++·msvc·c++20
fengbingchun17 天前
C++20中头文件syncstream的使用
c++20
barbyQAQ21 天前
C++20投影、范围与视图
java·jvm·c++20
guangcheng0312q25 天前
C++20那些事之constexpr与comma expr
java·开发语言·c++20