C++11一些常用的特性(多线程)

C++11一些常用的特性(多线程)

一、处理日期和时间的 chrono 库

C++11 中提供了日期和时间相关的库 chrono,通过 chrono 库可以很方便地处理日期和时间,为程序的开发提供了便利。chrono 库主要包含三种类型的类:时间间隔duration、时钟clocks、时间点time point。

1. 时间间隔 duration
1.1 常用类成员

duration表示一段时间间隔,用来记录时间长度,可以表示几秒、几分钟、几个小时的时间间隔。duration 的原型如下:

// 定义于头文件 <chrono>
template<
    class Rep,
    class Period = std::ratio<1>
> class duration;

Rep:这是一个数值类型,表示时钟数(周期)的类型(默认为整形)。若 Rep 是浮点数,则 duration 能使用小数描述时钟周期的数目。

Period:表示时钟的周期,它的原型如下:

// 定义于头文件 <ratio>
template<
    std::intmax_t Num,
    std::intmax_t Denom = 1
> class ratio;

ratio 类表示每个时钟周期的秒数,其中第一个模板参数 Num代表分子,Denom代表分母,该分母值默认为 1,因此,ratio代表的是一个分子除以分母的数值,比如:ratio<2> 代表一个时钟周期是 2 秒,ratio<60 > 代表一分钟,ratio<6060 > 代表一个小时,ratio<6060*24 > 代表一天。而 ratio<1,1000 > 代表的是 1/1000 秒,也就是 1 毫秒,ratio<1,1000000 > 代表一微秒,ratio<1,1000000000 > 代表一纳秒。

为了方便使用,在标准库中定义了一些常用的时间间隔,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于 chrono 命名空间下,定义如下:

纳秒:std::chrono::nanoseconds duration<Rep*/* 至少 64 位的有符号整数类型 /, std::nano>
微秒:std::chrono::microseconds duration<Rep*/* 至少 55 位的有符号整数类型 /, std::micro>
毫秒:std::chrono::milliseconds duration<Rep*/* 至少 45 位的有符号整数类型 /, std::milli>
秒: std::chrono::seconds duration<Rep*/* 至少 35 位的有符号整数类型 />
分钟:std::chrono::minutes duration<Rep*/* 至少 29 位的有符号整数类型 /, std::ratio<60>>
小时:std::chrono::hours duration<Rep*/* 至少 23 位的有符号整数类型 /, std::ratio<3600>>

注意:到 hours 为止的每个预定义时长类型至少涵盖 ±292 年的范围。

duration 类的构造函数原型如下:

// 1. 拷贝构造函数
duration( const duration& ) = default;
// 2. 通过指定时钟周期的类型来构造对象
template< class Rep2 >
constexpr explicit duration( const Rep2& r );
// 3. 通过指定时钟周期类型,和时钟周期长度来构造对象
template< class Rep2, class Period2 >
constexpr duration( const duration<Rep2,Period2>& d );

为了更加方便的进行 duration 对象之间的操作,类内部进行了操作符重载:

operator= 赋值内容 (公开成员函数)
operator+
operator- 实现一元 + 和一元 - (公开成员函数)
operator++
operator++(int)
operator–
operator–(int) 递增或递减周期计数 (公开成员函数)
operator+=
operator-=
operator*=
operator/=
operator%= 实现二个时长间的复合赋值 (公开成员函数)

duration 类还提供了获取时间间隔的时钟周期数的方法 count (),函数原型如下:

constexpr rep count() const;
1.2 类的使用

通过构造函数构造事件间隔对象示例代码如下:

#include <chrono>
#include <iostream>
using namespace std;
int main()
{
    chrono::hours h(1);                          // 一小时
    chrono::milliseconds ms{ 3 };                // 3 毫秒
    chrono::duration<int, ratio<1000>> ks(3);    // 3000 秒

    // chrono::duration<int, ratio<1000>> d3(3.5);  // error
    chrono::duration<double> dd(6.6);               // 6.6 秒

    // 使用小数表示时钟周期的次数
    chrono::duration<double, std::ratio<1, 30>> hz(3.5);
}

h(1) 时钟周期为 1 小时,共有 1 个时钟周期,所以 h 表示的时间间隔为 1 小时
ms(3) 时钟周期为 1 毫秒,共有 3 个时钟周期,所以 ms 表示的时间间隔为 3 毫秒
ks(3) 时钟周期为 1000 秒,一共有三个时钟周期,所以 ks 表示的时间间隔为 3000 秒
d3(3.5) 时钟周期为 1000 秒,时钟周期数量只能用整形来表示,但是此处指定的是浮点数,因此语法错误
dd(6.6) 时钟周期为默认的 1 秒,共有 6.6 个时钟周期,所以 dd 表示的时间间隔为 6.6 秒
hz(3.5) 时钟周期为 1/30 秒,共有 3.5 个时钟周期,所以 hz 表示的时间间隔为 1/30*3.5 秒
chrono 库中根据 duration 类封装了不同长度的时钟周期(也可以自定义),基于这个时钟周期再进行周期次数的设置就可以得到总的时间间隔了(时钟周期 * 周期次数 = 总的时间间隔)。

示例代码如下:

#include <chrono>
#include <iostream>
int main()
{
    std::chrono::milliseconds ms{3};         // 3 毫秒
    std::chrono::microseconds us = 2*ms;     // 6000 微秒
    // 时间间隔周期为 1/30 秒
    std::chrono::duration<double, std::ratio<1, 30>> hz(3.5);
 
    std::cout <<  "3 ms duration has " << ms.count() << " ticks\n"
              <<  "6000 us duration has " << us.count() << " ticks\n"
              <<  "3.5 hz duration has " << hz.count() << " ticks\n";       
}

输出的结果为:

3 ms duration has 3 ticks
6000 us duration has 6000 ticks
3.5 hz duration has 3.5 ticks

ms 时间单位为毫秒,初始化操作 ms{3} 表示时间间隔为 3 毫秒,一共有 3 个时间周期,每个周期为 1 毫秒
us 时间单位为微秒,初始化操作 2ms 表示时间间隔为 6000 微秒,一共有 6000 个时间周期,每个周期为 1 微秒
hz 时间单位为秒,初始化操作 hz(3.5) 表示时间间隔为 1/30
3.5 秒,一共有 3.5 个时间周期,每个周期为 1/30 秒

由于在 duration 类内部做了操作符重载,因此时间间隔之间可以直接进行算术运算,比如我们要计算两个时间间隔的差值,就可以在代码中做如下处理:

#include <iostream>
#include <chrono>
using namespace std;

int main()
{
    chrono::minutes t1(10);
    chrono::seconds t2(60);
    chrono::seconds t3 = t1 - t2;
    cout << t3.count() << " second" << endl;
}

程序输出的结果:

540 second

在上面的测试程序中,t1 代表 10 分钟,t2 代表 60 秒,t3 是 t1 减去 t2,也就是 60*10-60=540,这个 540 表示的时钟周期,每个时钟周期是 1 秒,因此两个时间间隔之间的差值为 540 秒。

注意事项:duration 的加减运算有一定的规则,当两个 duration 时钟周期不相同的时候,会先统一成一种时钟,然后再进行算术运算,统一的规则如下:假设有 ratio<x1,y1> 和 ratio<x2,y2 > 两个时钟周期,首先需要求出 x1,x2 的最大公约数 X,然后求出 y1,y2 的最小公倍数 Y,统一之后的时钟周期 ratio 为 ratio<X,Y>。

#include <iostream>
#include <chrono>
using namespace std;

int main()
{
    chrono::duration<double, ratio<9, 7>> d1(3);
    chrono::duration<double, ratio<6, 5>> d2(1);
    // d1 和 d2 统一之后的时钟周期
    chrono::duration<double, ratio<3, 35>> d3 = d1 - d2;
}

对于分子 6,、9 最大公约数为 3,对于分母 7、5 最小公倍数为 35,因此推导出的时钟周期为 ratio<3,35>

2. 时间点 time point

chrono 库中提供了一个表示时间点的类 time_point,该类的定义如下:

// 定义于头文件 <chrono>
template<
    class Clock,
    class Duration = typename Clock::duration
> class time_point;

它被实现成如同存储一个 Duration 类型的自 Clock 的纪元起始开始的时间间隔的值,通过这个类最终可以得到时间中的某一个时间点。

Clock:此时间点在此时钟上计量
Duration:用于计量从纪元起时间的 std::chrono::duration 类型
time_point 类的构造函数原型如下:

// 1. 构造一个以新纪元(epoch,即:1970.1.1)作为值的对象,需要和时钟类一起使用,不能单独使用该无参构造函数
time_point();
// 2. 构造一个对象,表示一个时间点,其中d的持续时间从epoch开始,需要和时钟类一起使用,不能单独使用该构造函数
explicit time_point( const duration& d );
// 3. 拷贝构造函数,构造与t相同时间点的对象,使用的时候需要指定模板参数
template< class Duration2 >
time_point( const time_point<Clock,Duration2>& t );

在这个类中除了构造函数还提供了另外一个 time_since_epoch() 函数,用来获得 1970 年 1 月 1 日到 time_point 对象中记录的时间经过的时间间隔(duration),函数原型如下:
duration time_since_epoch() const;

除此之外,时间点 time_point 对象和时间段对象 duration 之间还支持直接进行算术运算(即加减运算),时间点对象之间可以进行逻辑运算,具体细节可以参考下面的表格:

其中 tp 和 tp2 是 time_point 类型的对象, dtn 是 duration 类型的对象。

复合赋值 (成员函数) operator+= tp += dtn *this
复合赋值 (成员函数) operator-= tp -= dtn *this
算术运算符 (非成员函数) operator+ tp + dtn a time_point value
算术运算符 (非成员函数) operator+ dtn + tp atime_point value
算术运算符 (非成员函数) operator- tp - dtn a time_point value
算术运算符 (非成员函数) operator- tp - tp2 aduration value
关系操作符 (非成员函数) operator== tp == tp2 a bool value
关系操作符 (非成员函数) operator!= tp != tp2 a bool value
关系操作符 (非成员函数) operator< tp < tp2 a bool value
关系操作符 (非成员函数) operator> tp > tp2 a bool value
关系操作符 (非成员函数) operator>= tp >= tp2 a bool value
关系操作符 (非成员函数) operator<= tp <= tp2 a bool value

由于该时间点类经常和下面要介绍的时钟类一起使用,所以在此先不举例,在时钟类的示例代码中会涉及到时间点类的使用,到此为止只需要搞明白时间点类的提供的这几个函数的作用就可以了。

3. 时钟 clocks

chrono 库中提供了获取当前的系统时间的时钟类,包含的时钟一共有三种:

system_clock:系统的时钟,系统的时钟可以修改,甚至可以网络对时,因此使用系统时间计算时间差可能不准。
steady_clock:是固定的时钟,相当于秒表。开始计时后,时间只会增长并且不能修改,适合用于记录程序耗时
high_resolution_clock:和时钟类 steady_clock 是等价的(是它的别名)。
在这些时钟类的内部有 time_point、duration、Rep、Period 等信息,基于这些信息来获取当前时间,以及实现 time_t 和 time_point 之间的相互转换。

rep 表示时钟周期次数的有符号算术类型
period 表示时钟计次周期的 std::ratio 类型
duration 时间间隔,可以表示负时长
time_point 表示在当前时钟里边记录的时间点

在使用chrono提供的时钟类的时候,不需要创建类对象,直接调用类的静态方法就可以得到想要的时间了。

3.1 system_clock

具体来说,时钟类 system_clock 是一个系统范围的实时时钟。system_clock 提供了对当前时间点 time_point 的访问,将得到时间点转换为 time_t 类型的时间对象,就可以基于这个时间对象获取到当前的时间信息了。

system_clock 时钟类在底层源码中的定义如下:

struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
    using rep                       = long long;
    using period                    = ratio<1, 10'000'000>; // 100 nanoseconds
    using duration                  = chrono::duration<rep, period>;
    using time_point                = chrono::time_point<system_clock>;
    static constexpr bool is_steady = false;

    _NODISCARD static time_point now() noexcept 
    { // get current time
        return time_point(duration(_Xtime_get_ticks()));
    }

    _NODISCARD static __time64_t to_time_t(const time_point& _Time) noexcept 
    { // convert to __time64_t
        return duration_cast<seconds>(_Time.time_since_epoch()).count();
    }

    _NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept 
    { // convert from __time64_t
        return time_point{seconds{_Tm}};
    }
};

通过以上源码可以了解到在 system_clock 类中的一些细节信息:

rep:时钟周期次数是通过整形来记录的 long long
period:一个时钟周期是 100 纳秒 ratio<1, 10’000’000>
duration:时间间隔为 rep*period 纳秒 chrono::duration<rep, period>
time_point:时间点通过系统时钟做了初始化 chrono::time_point<system_clock>,里面记录了新纪元时间点

另外还可以看到 system_clock 类一共提供了三个静态成员函数:

// 返回表示当前时间的时间点。
static std::chrono::time_point<std::chrono::system_clock> now() noexcept;
// 将 time_point 时间点类型转换为 std::time_t 类型
static std::time_t to_time_t( const time_point& t ) noexcept;
// 将 std::time_t 类型转换为 time_point 时间点类型
static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;

比如,我们要获取当前的系统时间,并且需要将其以能够识别的方式打印出来,示例代码如下:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 新纪元1970.1.1时间
    system_clock::time_point epoch;

    duration<int, ratio<60*60*24>> day(1);
    // 新纪元1970.1.1时间 + 1天
    system_clock::time_point ppt(day);

    using dday = duration<int, ratio<60 * 60 * 24>>;
    // 新纪元1970.1.1时间 + 10天
    time_point<system_clock, dday> t(dday(10));

    // 系统当前时间
    system_clock::time_point today = system_clock::now();
    
    // 转换为time_t时间类型
    time_t tm = system_clock::to_time_t(today);
    cout << "今天的日期是:    " << ctime(&tm);

    time_t tm1 = system_clock::to_time_t(today+day);
    cout << "明天的日期是:    " << ctime(&tm1);

    time_t tm2 = system_clock::to_time_t(epoch);
    cout << "新纪元时间:      " << ctime(&tm2);

    time_t tm3 = system_clock::to_time_t(ppt);
    cout << "新纪元时间+1天:  " << ctime(&tm3);

    time_t tm4 = system_clock::to_time_t(t);
    cout << "新纪元时间+10天: " << ctime(&tm4);
}

示例代码打印的结果为:

今天的日期是:    Thu Apr  8 11:09:49 2021
明天的日期是:    Fri Apr  9 11:09:49 2021
新纪元时间:      Thu Jan  1 08:00:00 1970
新纪元时间+1天:  Fri Jan  2 08:00:00 1970
新纪元时间+10天: Sun Jan 11 08:00:00 1970
3.2 steady_clock

如果我们通过时钟不是为了获取当前的系统时间,而是进行程序耗时的时长,此时使用 syetem_clock 就不合适了,因为这个时间可以跟随系统的设置发生变化。在 C++11 中提供的时钟类 steady_clock 相当于秒表,只要启动就会进行时间的累加,并且不能被修改,非常适合于进行耗时的统计。

steady_clock 时钟类在底层源码中的定义如下:

struct steady_clock { // wraps QueryPerformanceCounter
    using rep                       = long long;
    using period                    = nano;
    using duration                  = nanoseconds;
    using time_point                = chrono::time_point<steady_clock>;
    static constexpr bool is_steady = true;

    // get current time
    _NODISCARD static time_point now() noexcept 
    { 
        // doesn't change after system boot
        const long long _Freq = _Query_perf_frequency(); 
        const long long _Ctr  = _Query_perf_counter();
        static_assert(period::num == 1, "This assumes period::num == 1.");
        const long long _Whole = (_Ctr / _Freq) * period::den;
        const long long _Part  = (_Ctr % _Freq) * period::den / _Freq;
        return time_point(duration(_Whole + _Part));
    }
};

通过以上源码可以了解到在 steady_clock 类中的一些细节信息:

rep:时钟周期次数是通过整形来记录的 long long
period:一个时钟周期是 1 纳秒 nano
duration:时间间隔为 1 纳秒 nanoseconds
time_point:时间点通过系统时钟做了初始化 chrono::time_point<steady_clock>
另外,在这个类中也提供了一个静态的 now () 方法,用于得到当前的时间点,函数原型如下:

static std::chrono::time_point<std::chrono::steady_clock> now() noexcept;

假设要测试某一段程序的执行效率,可以计算它执行期间消耗的总时长,示例代码如下:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 获取开始时间点
    steady_clock::time_point start = steady_clock::now();
    // 执行业务流程
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
    // 获取结束时间点
    steady_clock::time_point last = steady_clock::now();
    // 计算差值
    auto dt = last - start;
    cout << "总共耗时: " << dt.count() << "纳秒" << endl;
}
3.3 high_resolution_clock

high_resolution_clock 提供的时钟精度比 system_clock 要高,它也是不可以修改的。在底层源码中,这个类其实是 steady_clock 类的别名。

using high_resolution_clock = steady_clock;

因此 high_resolution_clock 的使用方式和 steady_clock 是一样的.

4. 转换函数
4.1 duration_cast

duration_cast 是 chrono 库提供的一个模板函数,这个函数不属于 duration 类。通过这个函数可以对 duration 类对象内部的时钟周期 Period,和周期次数的类型 Rep 进行修改,该函数原型如下:

template <class ToDuration, class Rep, class Period>
  constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);

在源周期能准确地为目标周期所整除的场合(例如小时到分钟),浮点时长和整数时长间转型能隐式进行无需使用 duration_cast ,其他情况下都需要通过函数进行转换。

我们可以修改一下上面测试程序执行时间的代码,在代码中修改 duration 对象的属性:

#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;

void f()
{
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
}

int main()
{
    auto t1 = steady_clock::now();
    f();
    auto t2 = steady_clock::now();

    // 整数时长:要求 duration_cast
    auto int_ms = duration_cast<chrono::milliseconds>(t2 - t1);

    // 小数时长:不要求 duration_cast
    duration<double, ratio<1, 1000>> fp_ms = t2 - t1;

    cout << "f() took " << fp_ms.count() << " ms, "
        << "or " << int_ms.count() << " whole milliseconds\n";
}

示例代码输出的结果:

print 1000 stars ....
****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
f() took 40.2547 ms, or 40 whole milliseconds
4.2 time_point_cast

time_point_cast 也是 chrono 库提供的一个模板函数,这个函数不属于 time_point 类。函数的作用是对时间点进行转换,因为不同的时间点对象内部的时钟周期 Period,和周期次数的类型 Rep 可能也是不同的,一般情况下它们之间可以进行隐式类型转换,也可以通过该函数显示的进行转换,函数原型如下:

template <class ToDuration, class Clock, class Duration>
time_point<Clock, ToDuration> time_point_cast(const time_point<Clock, Duration> &t);

关于函数的使用,示例代码如下:

#include <chrono>
#include <iostream>
using namespace std;

using Clock = chrono::high_resolution_clock;
using Ms = chrono::milliseconds;
using Sec = chrono::seconds;
template<class Duration>
using TimePoint = chrono::time_point<Clock, Duration>;

void print_ms(const TimePoint<Ms>& time_point)
{
    std::cout << time_point.time_since_epoch().count() << " ms\n";
}

int main()
{
    TimePoint<Sec> time_point_sec(Sec(6));
    // 无精度损失, 可以进行隐式类型转换
    TimePoint<Ms> time_point_ms(time_point_sec);
    print_ms(time_point_ms);    // 6000 ms

    time_point_ms = TimePoint<Ms>(Ms(6789));
    // error,会损失精度,不允许进行隐式的类型转换
    TimePoint<Sec> sec(time_point_ms);

    // 显示类型转换,会损失精度。6789 truncated to 6000
    time_point_sec = std::chrono::time_point_cast<Sec>(time_point_ms);
    print_ms(time_point_sec); // 6000 ms
}

注意事项:关于时间点的转换如果没有没有精度的损失可以直接进行隐式类型转换,如果会损失精度只能通过显示类型转换,也就是调用 time_point_cast 函数来完成该操作

二、C++ 线程的使用

C++11 之前,C++ 语言没有对并发编程提供语言级别的支持,这使得我们在编写可移植的并发程序时,存在诸多的不便。现在 C++11 中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。

C++11 中提供的线程类叫做 std::thread,基于这个类创建一个新的线程非常的简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。我们首先来了解一下这个类提供的一些常用 API:

1. 构造函数
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;

构造函数①:默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作

构造函数②:移动构造函数,将 other 的线程所有权转移给新的 thread 对象。之后 other 不再表示执行线程。

构造函数③:创建线程对象,并在该线程中执行函数 f 中的业务逻辑,args 是要传递给函数 f 的参数

任务函数 f 的可选类型有很多,具体如下:

普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型)
可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
构造函数④:使用 =delete 显示删除拷贝构造,不允许线程对象之间的拷贝

2. 公共成员函数

2.1 get_id()
应用程序启动之后默认只有一个线程,这个线程一般称之为主线程或父线程,通过线程类创建出的线程一般称之为子线程,每个被创建出的线程实例都对应一个线程 ID,这个 ID 是唯一的,可以通过这个 ID 来区分和识别各个已经存在的线程实例,这个获取线程 ID 的函数叫做 get_id(),函数原型如下:

std::thread::id get_id() const noexcept;

示例程序如下:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func(int num, string str)
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "子线程: i = " << i << "num: " 
             << num << ", str: " << str << endl;
    }
}

void func1()
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "子线程: i = " << i << endl;
    }
}

int main()
{
    cout << "主线程的线程ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "线程t 的线程ID: " << t.get_id() << endl;
    cout << "线程t1的线程ID: " << t1.get_id() << endl;
}

thread t(func, 520, “i love you”);:创建了子线程对象 t,func() 函数会在这个子线程中运行
func() 是一个回调函数,线程启动之后就会执行这个任务函数,程序猿只需要实现即可
func() 的参数是通过 thread 的参数进行传递的,520,i love you 都是调用 func() 需要的实参
线程类的构造函数③是一个变参函数,因此无需担心线程任务函数的参数个数问题
任务函数 func() 一般返回值指定为 void,因为子线程在调用这个函数的时候不会处理其返回值
thread t1(func1);:子线程对象 t1 中的任务函数 func1(),没有参数,因此在线程构造函数中就无需指定了
通过线程对象调用 get_id() 就可以知道这个子线程的线程 ID 了,t.get_id(),t1.get_id()。

在上面的示例程序中有一个 bug,在主线程中依次创建出两个子线程,打印两个子线程的线程 ID,最后主线程执行完毕就退出了(主线程就是执行 main () 函数的那个线程)。默认情况下,主线程销毁时会将与其关联的两个子线程也一并销毁,但是这时有可能子线程中的任务还没有执行完毕,最后也就得不到我们想要的结果了。

当启动了一个线程(创建了一个 thread 对象)之后,在这个线程结束的时候(std::terminate ()),我们如何去回收线程所使用的资源呢?thread 库给我们两种选择:

加入式(join())
分离式(detach())
另外,我们必须要在线程对象销毁之前在二者之间作出选择,否则程序运行期间就会有 bug 产生。

2.2 join()

join() 字面意思是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调用 join() 函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后 join() 会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。

再次强调,我们一定要搞清楚这个函数阻塞的是哪一个线程,函数在哪个线程中被执行,那么函数就阻塞哪个线程。该函数的函数原型如下:

void join();

有了这样一个线程阻塞函数之后,就可以解决在上面测试程序中的 bug 了,如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法即可,当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除了。修改之后的示例代码如下:

int main()
{
    cout << "主线程的线程ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "线程t 的线程ID: " << t.get_id() << endl;
    cout << "线程t1的线程ID: " << t1.get_id() << endl;
    t.join();
    t1.join();
}

当主线程运行到第八行 t.join();,根据子线程对象 t 的任务函数 func() 的执行情况,主线程会做如下处理:

如果任务函数 func() 还没执行完毕,主线程阻塞,直到任务执行完毕,主线程解除阻塞,继续向下运行
如果任务函数 func() 已经执行完毕,主线程不会阻塞,继续向下运行
同样,第 9 行的代码亦如此。

为了更好的理解 join() 的使用,再来给大家举一个例子,场景如下:

程序中一共有三个线程,其中两个子线程负责分段下载同一个文件,下载完毕之后,由主线程对这个文件进行下一步处理,那么示例程序就应该这么写:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void download1()
{
    // 模拟下载, 总共耗时500ms,阻塞线程500ms
    this_thread::sleep_for(chrono::milliseconds(500));
    cout << "子线程1: " << this_thread::get_id() << ", 找到历史正文...." << endl;
}

void download2()
{
    // 模拟下载, 总共耗时300ms,阻塞线程300ms
    this_thread::sleep_for(chrono::milliseconds(300));
    cout << "子线程2: " << this_thread::get_id() << ", 找到历史正文...." << endl;
}

void doSomething()
{
    cout << "集齐历史正文, 呼叫罗宾...." << endl;
    cout << "历史正文解析中...." << endl;
    cout << "起航,前往拉夫德尔...." << endl;
    cout << "找到OnePiece, 成为海贼王, 哈哈哈!!!" << endl;
    cout << "若干年后,草帽全员卒...." << endl;
    cout << "大海贼时代再次被开启...." << endl;
}

int main()
{
    thread t1(download1);
    thread t2(download2);
    // 阻塞主线程,等待所有子线程任务执行完毕再继续向下执行
    t1.join();
    t2.join();
    doSomething();
}

示例程序输出的结果:

子线程2: 72540, 找到历史正文....
子线程1: 79776, 找到历史正文....
集齐历史正文, 呼叫罗宾....
历史正文解析中....
起航,前往拉夫德尔....
找到OnePiece, 成为海贼王, 哈哈哈!!!
若干年后,草帽全员卒....
大海贼时代再次被开启....

在上面示例程序中最核心的处理是在主线程调用 doSomething(); 之前在第 35、36行通过子线程对象调用了 join() 方法,这样就能够保证两个子线程的任务都执行完毕了,也就是文件内容已经全部下载完成,主线程再对文件进行后续处理,如果子线程的文件没有下载完毕,主线程就去处理文件,很显然从逻辑上讲是有问题的。

2.3 detach()

detach() 函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。(其实就是孩子翅膀硬了,和家里断绝关系,自己外出闯荡了,如果家里被诛九族还是会受牵连)。该函数函数原型如下:

void detach();

线程分离函数没有参数也没有返回值,只需要在线程成功之后,通过线程对象调用该函数即可,继续将上面的测试程序修改一下:

int main()
{
    cout << "主线程的线程ID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "线程t 的线程ID: " << t.get_id() << endl;
    cout << "线程t1的线程ID: " << t1.get_id() << endl;
    t.detach();
    t1.detach();
    // 让主线程休眠, 等待子线程执行完毕
    this_thread::sleep_for(chrono::seconds(5));
}

注意事项:线程分离函数 detach () 不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了,比如:通过 join () 阻塞主线程等待子线程中的任务执行完毕,或者调用 get_id () 获取子线程的线程 ID。有利就有弊,鱼和熊掌不可兼得,建议使用 join ()。

2.4 joinable()

joinable() 函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

返回值为 true:主线程和子线程之间有关联(连接)关系
返回值为 false:主线程和子线程之间没有关联(连接)关系

bool joinable() const noexcept;

示例代码如下:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void foo()
{
    this_thread::sleep_for(std::chrono::seconds(1));
}

int main()
{
    thread t;
    cout << "before starting, joinable: " << t.joinable() << endl;

    t = thread(foo);
    cout << "after starting, joinable: " << t.joinable() << endl;

    t.join();
    cout << "after joining, joinable: " << t.joinable() << endl;

    thread t1(foo);
    cout << "after starting, joinable: " << t1.joinable() << endl;
    t1.detach();
    cout << "after detaching, joinable: " << t1.joinable() << endl;
}

示例代码打印的结果如下:

before starting, joinable: 0
after starting, joinable: 1
after joining, joinable: 0
after starting, joinable: 1
after detaching, joinable: 0

基于示例代码打印的结果可以得到以下结论:

在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接
在创建的子线程对象的时候,如果指定了任务函数,子线程启动并执行任务,主线程和这个子线程自动连接成功
子线程调用了detach()函数之后,父子线程分离,同时二者的连接断开,调用joinable()返回false
在子线程调用了join()函数,子线程中的任务函数继续执行,直到任务处理完毕,这时join()会清理(回收)当前子线程的相关资源,所以这个子线程和主线程的连接也就断开了,因此,调用join()之后再调用joinable()会返回false。

2.5 operator=

线程中的资源是不能被复制的,因此通过 = 操作符进行赋值操作最终并不会得到两个完全相同的对象。

// move (1)	
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)	
thread& operator= (const other&) = delete;

通过以上 = 操作符的重载声明可以得知:

如果 other 是一个右值,会进行资源所有权的转移
如果 other 不是右值,禁止拷贝,该函数被显示删除(=delete),不可用

3. 静态函数

thread 线程类还提供了一个静态方法,用于获取当前计算机的 CPU 核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

static unsigned hardware_concurrency() noexcept;

示例代码如下:

#include <iostream>
#include <thread>
using namespace std;

int main()
{
    int num = thread::hardware_concurrency();
    cout << "CPU number: " << num << endl;
}

三、命名空间 - this_thread

在 C++11 中不仅添加了线程类,还添加了一个关于线程的命名空间 std::this_thread,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。

1. get_id()

调用命名空间 std::this_thread 中的 get_id() 方法可以得到当前线程的线程 ID,函数原型如下:

thread::id get_id() noexcept;

关于函数使用对应的示例代码如下:

#include <iostream>
#include <thread>
using namespace std;

void func()
{
    cout << "子线程: " << this_thread::get_id() << endl;
}

int main()
{
    cout << "主线程: " << this_thread::get_id() << endl;
    thread t(func);
    t.join();
}

程序启动,开始执行 main() 函数,此时只有一个线程也就是主线程。当创建了子线程对象 t 之后,指定的函数 func() 会在子线程中执行,这时通过调用 this_thread::get_id() 就可以得到当前线程的线程 ID 了。

2. sleep_for()

同样地线程被创建后也有这五种状态:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态) ,关于状态之间的转换是一样的,请参考进程,在此不再过多的赘述。

线程和进程的执行有很多相似之处,在计算机中启动的多个线程都需要占用 CPU 资源,但是 CPU 的个数是有限的并且每个 CPU 在同一时间点不能同时处理多个任务。为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到 CPU 时间片的情况)。

命名空间 this_thread 中提供了一个休眠函数 sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了 CPU 资源,代码也不会被执行,所以线程休眠过程中对 CPU 来说没有任何负担。这个函数是函数原型如下,参数需要指定一个休眠时长,是一个时间段:

template <class Rep, class Period>
  void sleep_for (const chrono::duration<Rep,Period>& rel_time);

示例程序如下:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func()
{
    for (int i = 0; i < 10; ++i)
    {
        this_thread::sleep_for(chrono::seconds(1));
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
    }
}

int main()
{
    thread t(func);
    t.join();
}

在 func() 函数的 for 循环中使用了 this_thread::sleep_for(chrono::seconds(1)); 之后,每循环一次程序都会阻塞 1 秒钟,也就是说每隔 1 秒才会进行一次输出。需要注意的是:程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢 CPU 时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。

3. sleep_until()

命名空间 this_thread 中提供了另一个休眠函数 sleep_until(),和 sleep_for() 不同的是它的参数类型不一样

sleep_until():指定线程阻塞到某一个指定的时间点 time_point类型,之后解除阻塞
sleep_for():指定线程阻塞一定的时间长度 duration 类型,之后解除阻塞

template <class Clock, class Duration>
  void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);

示例程序如下:

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func()
{
    for (int i = 0; i < 10; ++i)
    {
        // 获取当前系统时间点
        auto now = chrono::system_clock::now();
        // 时间间隔为2s
        chrono::seconds sec(2);
        // 当前时间点之后休眠两秒
        this_thread::sleep_until(now + sec);
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
    }
}

int main()
{
    thread t(func);
    t.join();
}

sleep_until() 和 sleep_for() 函数的功能是一样的,只不过前者是基于时间点去阻塞线程,后者是基于时间段去阻塞线程,项目开发过程中根据实际情况选择最优的解决方案即可。

4. yield()

命名空间 this_thread 中提供了一个非常绅士的函数 yield(),在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的 CPU 时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到 CPU 时间片了。使用这个函数的时候需要注意一点,线程调用了 yield () 之后会主动放弃 CPU 资源,但是这个变为就绪态的线程会马上参与到下一轮 CPU 的抢夺战中,不排除它能继续抢到 CPU 时间片的情况,这是概率问题。

void yield() noexcept;

函数对应的示例程序如下:

#include <iostream>
#include <thread>
using namespace std;

void func()
{
    for (int i = 0; i < 100000000000; ++i)
    {
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
        this_thread::yield();
    }
}

int main()
{
    thread t(func);
    thread t1(func);
    t.join();
    t1.join();
}

在上面的程序中,执行 func() 中的 for 循环会占用大量的时间,在极端情况下,如果当前线程占用 CPU 资源不释放就会导致其他线程中的任务无法被处理,或者该线程每次都能抢到 CPU 时间片,导致其他线程中的任务没有机会被执行。解决方案就是每执行一次循环,让该线程主动放弃 CPU 资源,重新和其他线程再次抢夺 CPU 时间片,如果其他线程抢到了 CPU 时间片就可以执行相应的任务了。

结论:

std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降
std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢

作者: 苏丙榅
链接: https://subingwen.cn/cpp/this_thread/#4-yield
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值