多线程 /C++ 11 std::thread 类深入理解和应用实践

概述

C++11 的引入标志着多线程编程成为 C++ 标准的一部分,使得多线程编程更加便捷和跨平台。在此之前,要实现多线程编程需要使用操作系统特定的API或第三方库。C++11 还引入了其他与多线程编程相关的特性,如原子操作、互斥量、条件变量等,以支持线程间的同步和互斥访问。本文基于 C++ Thread 帮助文档,结合日常实践,总结 std::thread 的使用方式和注意事项。一些基础使用可参考《菜鸟教程 C++ std::thread》,此文不赘述。

准确理解 joinable 属性

也不知道从哪里看到如下这么一句错误的话,误导了我很久:
" 在声明一个std::thread对象之后,都可以使用detach和join函数来启动被调线程,区别在于两者是否阻塞主调线程 。"
下文将结合对帮助文档中诸多细节的理解,来说明上述说辞的错误原因,并正确理解 joinable 属性。

a thread of execution 执行线程

Class to represent individual threads of execution.(类std::thread代表独立执行的线程)A thread of execution is a sequence of instructions that can be executed concurrently with other such sequences in multithreading environments, while sharing a same address space. (执行线程是指:在多线程环境下,可以与其它类似指令序列同时执行且共享’进程’地址空间的一个指令序列)
这里的解释有点过于学术。我们知道,线程是操作系统调度的基本单位,是进程内的执行流程,它是操作系统层级的概念。而执行线程,通常指的是在编程语言和库级别上表示的执行单元。如上所述,被初始化过的std::thread类对象就代表执行线程。

线程 active

An initialized thread object represents an active thread of execution; Such a thread object is joinable, and has a unique thread id. (线程对象初始化后即可以代表活动的执行线程,这样的线程对象是可链接的,它具有唯一线程ID)。也是在 “后来(第一次阅读这个属性HelpDoc的3个月后)” 我才明白了这句话的意思,明白了什么是 ‘活动的线程’:

bool m_bRunningFlag = true;
void ProcessOfStdThread(void)
{
    while (m_bRunningFlag) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        printf("process is running..\r\n");
    }
}
int main()
{
    std::thread threadX = std::thread(&ProcessOfStdThread);
    system("pause"); return 0;
}

经过上述测试,我们可以知道,threadX入口函数的启动,并不需要调用 函数join 或 函数detach,而只是执行了对象创建操作,ProcessOfStdThread 便进行了’执行状态’。也就是Help中所说的活动状态

线程 joinable “可加入”

Thread 类属性 joinable 贯穿了Help Doc的始终。从表层意思来看,它是"可链接的、可加入的",网络释义中 joinable Thread 甚至可以解释为"等待线程"。那么,它到底是什么?

/* default*/          thread() noexcept;  //默认构造函数 
/* initialization */  template <class Fn, class... Args>explicit thread (Fn&& fn, Args&&... args); //初始化构造函数
/* copy [deleted] */  thread (const thread&) = delete;  //表示复制构造函数是被禁用的
/* move */            thread (thread&& x) noexcept;  //移动构造函数

A default-constructed (non-initialized) thread object is not joinable, and its thread id is common for all non-joinable threads. 使用默认构造函数构造的线程对象是不可连接的,所有不可连接的线程对象其线程ID是一致的。因为默认构造函数连入口函数都不指定,而初始化构造函数和移动构造函数都是有指定入口函数的。A joinable thread becomes not joinable if moved from, or if either join or detach are called on them. 一个可连接的线程对象在被移动、join函数调用、detach函数调用后将变的不可连接。

bool joinable() const noexcept;
//Check if joinable. Returns whether the thread object is joinable.

A thread object is joinable if it represents a thread of execution. A thread object is not joinable in any of these cases:

  1. if it was default-constructed.
  2. if it has been moved from (either constructing another thread object, or assigning to it).
  3. if either of its members join or detach has been called.

若将joinable翻译成"可连接的",那么上述Help句子可以理解为:可连接状态的std::thread对象代表一个执行线程,当对象是不可连接状态时,它不代表执行线程?理解上有点别扭。
个人认为将 joinable 翻译成 ”可加入的“,似乎更加合理。它指的是可加入到当前线程中,这里的当前线程可以是创建它的线程,也可以是其他的任意的线程;可以是主线程,也可以是其他次线程。重新理解上文中的 joinable 和 not joinable :
一个线程对象如果可以代表一个执行线程,那么他就是可加入到其他线程中的;默认构造函数不能代表执行线程,是不可加入的;被移动构造过的对象,或执行过一次join/detach操作的对象将变成不可加入的线程对象。

函数 join 和 函数 detach

调用到 join 语句的线程将被阻塞,直到 thread 入口函数退出执行,主调线程回收被调线程资源,“主线程” 得以继续运行。detach 函数使创建线程不用等待子线程结束,可以继续往下执行,即使 “主” 线程终止了;被调线程驻留后台运行,主调线程无法再取得该被调线程的控制权,当主调线程结束时,由运行时库负责清理与被调线程相关的资源。

The function returns when the thread execution has completed. This synchronizes the moment this function returns with the completion of all the operations in the thread: This blocks the execution of the thread that calls this function until the function called on construction returns (if it hasn’t yet). 函数join将在执行线程完成后返回,这将join函数返回和线程运行的结束同步起来:它阻塞join函数的调用线程直到在构造函数中调用的函数(这里指初始化构造函数中传入的线程入口函数)返回(如果尚未返回)。

int main()
{
    int interval_child = 1, interval_main = 2;  //s

    //
    std::thread t1 = std::thread([&]() {
        std::this_thread::sleep_for(std::chrono::seconds(interval_child));
        printf("at:%f, t1_entry_func return \n", DalOsTimeSysGetTime());
    });

    //等待次线程结束
    std::this_thread::sleep_for(std::chrono::seconds(interval_main));
    //
    printf("at:%f, main end sleep t1.joinable:%d \n", DalOsTimeSysGetTime(), t1.joinable());

    try {
        t1.join();
        printf("at:%f, t1.join return \n", DalOsTimeSysGetTime());
    }
    catch (const std::exception&) {
        std::cout << "any system_error exception \n";
    }

    system("pause");  return 0;
}

//测试1:
int interval_child = 1, interval_main = 2;  //s
//at:104904927.196700, t1_entry_func return
//at:104905927.851700, main end sleep t1.joinable:1
//at:104905928.098900, t1.join return

//测试2:
int interval_child = 3, interval_main = 1;  //s
//at:105283245.343900, main end sleep t1.joinable:1
//at:105285258.948800, t1_entry_func return
//at:105285259.560300, t1.join return

经上述代码测试,可得出:使用初始化构造函数创建的线程对象,即使线程入口函数执行返回了,只要没有进行过 join、detach、move 操作,它依然是joinable的,是可加入到其他线程的。

如何理解线程对象是被安全地销毁的

函数 join 和 函数 detach 的帮助文档中都讲到,
join(), After a call to this function, the thread object becomes non-joinable and can be destroyed safely.
detach(), After a call to std::thread::detach, the thread object becomes non-joinable and can be destroyed safely.
在线程对象上调用join函数或detach函数后,线程对象就可安全地销毁了,如何是安全地,真的安全吗?

参考 《多线程/WinAPI线程退出方式比较分析》可知,使得入口函数自然的返回而退出执行线程,是唯一优雅的线程退出方式。猜测这里的这里的"安全",与它应该是同一个意思。相关测试思路主要有:
1、调用join或detach可以使得执行安全退出,那么我们不调用他们的任何一个,便可理解何为不安全。
2、类似于对WinAPI线程退出的优雅测试,也对std::thread线程内局部类对象的析构函数是否会被调用,进行"安全"测试。
基于上述主要测试思路整理了《多线程/std::thread线程退出方式详解》相关内容。基本结论如下,
0、这里的安全,并不不是可靠的安全。
1、进程退出前,必须要使用 函数 join 或 函数 detach 中的一种方式来将线程加入到当前线程或从当前线程中分离出来,否则可能导致程序崩溃。
2、在 detach 作用下,并不会保证入口函数返回。当前线程退出时,如果入口函数已经完成,则是没有任何问题得到。但如果此时入口函数尚在执行过程中(如等待、耗时IO操作等),由于入口函数没有返回,此时并不会触发线程内类对象的析构过程,这与windowsAPI::ExitThread的使用效果如出一辙。
3、建议在使用std::thread进行多线程编程是,使用join函数等待目标次线程明确的返回退出。该问题可以参考《多线程 /std::thread /windows::thread /优雅并安全的退出线程执行》一文中的说明。

native_handle 线程句柄

在对比WinAPI多线程编程接口时候,还在考虑怎么没有一个类似WaitForSingleObject的接口来等待线程结束呢? 其实 native_handle 函数就间接的提供了这种功能。
This member function is only present in class thread if the library implementation supports it. If present, it returns a value used to access implementation-specific information associated to the thread.
如果你使用的C++库支持该成员函数,那么它返回一个用以访问本地线程的句柄。如下测试中,native_handle返回的句柄,可以用以WaitForSingleObject函数,以替代join过程。

int main()
{
    int interval_child = 3, interval_main = 1;  //s

    //
    std::thread t1 = std::thread([&]() {
        std::this_thread::sleep_for(std::chrono::seconds(interval_child));
        printf("at:%f, t1_entry_func return \n", DalOsTimeSysGetTime());
    });

    //等待次线程结束
    std::this_thread::sleep_for(std::chrono::seconds(interval_main));
    //
    printf("at:%f, main end sleep t1.joinable:%d \n", DalOsTimeSysGetTime(), t1.joinable());

    try {
        //t1.join();
        //printf("at:%f, t1.join return \n", DalOsTimeSysGetTime());
        void *H = t1.native_handle();
        DWORD dr = WaitForSingleObject(H, INFINITE);
        printf("at:%f, Windows WaitForSingleObject return \n", DalOsTimeSysGetTime());
        printf("at:%f, t1.joinable:%d \n", t1.joinable()); //依然是joinable的
    }
    catch (const std::exception&) {
        std::cout << "any system_error exception \n";
    }

    system("pause");  return 0;
}
//at:88937493.215309, main end sleep t1.joinable:1
//at:88939493.174349, t1_entry_func return
//at:88939493.701710, Windows WaitForSingleObject return
//t1.joinable:1

上述测试中,WaitForSingleObject (t1.native_handle…) 并没有改变 t1 的joinable特性,在进程退出前还是得执行加入或分离操作。因此,WaitForSingleObject (t1.native_handle…) 比较适合配合 detach 从当前线程分离执行线程的情况下使用。

std::thread类的其他接口

//基于lambda的初始化构造函数
std::thread t1 = std::thread([&]() {
    std::this_thread::sleep_for(std::chrono::seconds(interval_child));
    printf("at:%f, t1_entry_func return \n", DalOsTimeSysGetTime());
});
std::thread::hardware_concurrency()  //static
//主要作用是返回当前系统支持的并发线程数量的估计值。以便在编写并发程序时进行合理的线程数量规划。
std::thread::get_id()
//函数返回表示线程标识符的std::thread::id类型的对象,它可以用于判断两个线程对象是否表示同一个线程,或者用于与特定的线程标识符进行比较。

常见错误

错误 C2893

错误 C2893 未能使函数模板“unknown-type std::invoke(_Callable &&,_Types &&…)”专用化 XX_SDK d:\program files (x86)\microsoft visual studio 14.0\vc\include\thr\xthread 240
错误 C2672 “std::invoke”: 未找到匹配的重载函数 XX_SDK d:\program files (x86)\microsoft visual studio 14.0\vc\include\thr\xthread 240
警告 MSB8004 Output Directory does not end with a trailing slash. This build instance will add the slash as it is required to allow proper evaluation of the Output Directory.

//针对类成员函数 如果不附加this指针参数 将会有上述告警
std::thread m_thread = std::thread(&CTaskXXX::ProcessThread/*, this*/);

错误 C2653

错误 C2653 “this_thread”: 不是类或命名空间名称…

//错误定位在如下代码行 /已#include <thread>
this_thread::sleep_for(std::chrono::milliseconds(1));

尝试增加using namespace this_thread; 语句,无济于事,且本行就编辑器告警。最终做如下修改,恍然大悟。

std::this_thread::sleep_for(std::chrono::milliseconds(1));
//如果要直接使用this_thread命名空间,必须先using namespace std; /命名控件要使用全路径
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值