命名空间this_thread

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

get_id()

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

1
thread::id get_id() noexcept;

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <thread>
using namespace std;

void func()
{
cout << "child thread: " << this_thread::get_id() << endl;
}

int main()
{
cout << "main thread: " << this_thread::get_id() << endl;
thread t(func);
t.join();
}
// g++ get_id3.cpp -o app

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

sleep_for()

线程被创建后也有这五种状态:创建态、就绪态、运行态,阻塞态(挂起态)、退出态(终止态)。关于状态之间的转换是一样的,参考进程。
线程和进程的执行有很多相似之处,在计算机中启动的多个线程都需要占用 CPU 资源,但是 CPU 的个数是有限的并且每个 CPU 在同一时间点不能同时处理多个任务。

为了能够实现并发处理,多个线程都是分时复用 CPU 时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢 CPU 时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到 CPU 时间片的情况)。

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

1
2
3
4
5
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
/*
参数:指定一个休眠时长,是一个时间段
*/

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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 << "child thread: " << this_thread::get_id() << ", i: " << i << endl;
}
}

int main()
{
cout << "main thread: " << this_thread::get_id() << endl;
thread t(func);
t.join();
}
// g++ sleep_for.cpp -o app

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

sleep_until()

命名空间 this_thread 中提供了另一个休眠函数 sleep_until()sleep_for() 不同在于参数类型不一样。
sleep_until():指定线程阻塞到某一个指定的时间点 time_point 类型,之后解除阻塞。
sleep_for():指定线程阻塞一定的时间长度 duration 类型,之后解除阻塞。

函数原型如下:

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

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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 << "child thread: " << this_thread::get_id() << ", i: " << i << endl;
}
}

int main()
{
cout << "main thread: " << this_thread::get_id() << endl;
thread t(func);
t.join();
}
// g++ sleep_until.cpp -o app

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

yield()

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

函数原型如下:

1
void yield() noexcept;

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>
using namespace std;

void func()
{
for (int i = 0; i < 100; ++i)
{
cout << "child thread: " << this_thread::get_id() << ", i: " << i << endl;
this_thread::yield();
}
}

int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
}
// g++ yield.cpp -o app

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

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

参考资料

https://subingwen.cn/cpp/this_thread


命名空间this_thread
https://lcf163.github.io/2021/08/25/命名空间this-thread/
作者
乘风的小站
发布于
2021年8月25日
许可协议