get_id()、sleep_for()和sleep_until()、yield()、swap()和移动拷贝构造/赋值函数、call_once:多线程环境中,线程的任务函数中调用函数A,但只希望A被调用一次、C++11线程库获取与操作系统相关的原生线程句柄、原子类型
C++11提供了命名空间this_thread来表示当前线程,该命名空间中有四个函数:get_id()、sleep_for()、sleep_until()、yield()。
1. get_id()、sleep_for()和sleep_until()
this_thread::sleep_for(chrono::seconds(1));
:windows中使用Sleep用于睡眠,linux中使用sleep用于睡眠。c++11提供了跨平台的this_thread::sleep_for(chrono::seconds(1));
,其中chrono::seconds(1)
可以换成其他单位。下面是get_id()、sleep_for()和sleep_until()的举例:
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
// 普通函数。
void func(int bh, const string& str) {
cout << "子线程:" << this_thread::get_id() << endl;
for (int ii = 1; ii <= 3; ii++)
{
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main()
{
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
cout << "主线程:" << this_thread::get_id() << endl;
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
sleep_until()可让线程休眠至指定时间点(可实现定时任务),这里不做过多介绍。
2. yield()
该函数让线程主动让出自己已经抢到的CPU时间片。
// this_thread::yield example
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <atomic> // std::atomic
std::atomic<bool> ready (false);
void count1m(int id) {
while (!ready) { // wait until main() sets ready...
std::this_thread::yield();
}
for (volatile int i=0; i<1000000; ++i) {} // volatile防止编译器优化导致:有些线程从寄存器中取出变量i,有些线程从内存中取出i,这会导致i不一样。
std::cout << id;
}
int main ()
{
std::thread threads[10];
std::cout << "race of 10 threads that count to 1 million:\n";
for (int i=0; i<10; ++i) threads[i]=std::thread(count1m,i);
ready = true; // go!
for (auto& th : threads) th.join();
std::cout << '\n';
return 0;
}
3.swap()和移动拷贝构造/赋值函数
线程不可拷贝但是可以移动,如下使用swap进行交换操作。
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
// 普通函数。
void func(int bh, const string& str) {
cout << "子线程:" << this_thread::get_id() << endl;
for (int ii = 1; ii <= 3; ii++) {
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main() {
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
cout << "主线程:" << this_thread::get_id() << endl;
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;
t2.swap(t1); // 交换线程t1和线程t2。
cout << "线程t1:" << t1.get_id() << endl;
cout << "线程t2:" << t2.get_id() << endl;
thread t3 = move(t1); // 移动线程t1到线程t3。
cout << "线程t3:" << t3.get_id() << endl;
// t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
t3.join(); // 回收线程t3的资源。
}
上述代码中使用swap交换t1和t2的资源,并将t1的资源移动到到线程t3上,此时t1就不能使用了,所以这里将t1.join();
注释掉。
4.call_once:多线程环境中,线程的任务函数中调用函数A,但只希望A被调用一次。
#include <iostream>
#include <mutex> // std::once_flag和std::call_once()函数需要包含这个头文件。
#include <thread> // 线程类头文件。
using namespace std;
once_flag onceflag; // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
cout << "once_func() bh= " << bh << ", str=" << str << endl;
}
// 普通函数。
void func(int bh, const string& str) {
call_once(onceflag, once_func, 0, "各位观众,我要开始表白了。");
for (int ii = 1; ii <= 3; ii++) {
cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main() {
// 用普通函数创建线程。
thread t1(func, 3, "我是一只傻傻鸟。");
thread t2(func, 8, "我有一只小小鸟。");
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
}
5.C++11线程库获取与操作系统相关的原生线程句柄
C++11定义了线程标准,不同的平台和编译器在实现的时候,本质上都是对操作系统的线程库进行封装,会损失一部分功能。
为了弥补C++11线程库的不足,thread类提供了native_handle()成员函数,用于获得与操作系统相关的原生线程句柄,操作系统原生的线程库就可以用原生线程句柄操作线程。
#include <pthread.h> // Linux的pthread线程库头文件。
#include <iostream>
#include <thread>
using namespace std;
void func() // 线程任务函数。
{
for (int ii = 1; ii <= 10; ii++) {
cout << "ii=" << ii << endl;
this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒。
}
}
int main() {
thread tt(func); // 创建线程。
this_thread::sleep_for(chrono::seconds(5)); // 休眠5秒。
pthread_t thid = tt.native_handle(); // 获取Linux操作系统原生的线程句柄。
pthread_cancel(thid); // 取消线程。
tt.join(); // 等待线程退出。
}
上述代码中,tt由C++11的线程库thread进行创建的,但是C++11的线程库thread中并没有取消线程的操作。通过tt.native_handle()可以获取Linux操作系统原生的线程句柄,然后就可以通过使用linux的pthread线程库来操作此句柄,从而操作线程tt。
6.原子类型:
C++11提供了atomic
原子操作由CPU指令提供支持,它的性能比锁和消息传递更高,并且,不需要程序员处理加锁和释放锁的问题,支持修改、读取、交换、比较并交换等操作。
#include <atomic>
#include <iostream>
#include <thread> // 线程类头文件。
using namespace std;
atomic<int> aa(0); // 定义全局变量。
// 普通函数,把全局变量aa加1000000次。
void func() {
for (int ii = 1; ii <= 1000000; ii++) aa++;
}
int main() {
// 用普通函数创建线程。
thread t1(func); // 创建线程t1,把全局变量aa加1000000次。
thread t2(func); // 创建线程t2,把全局变量aa加1000000次。
t1.join(); // 回收线程t1的资源。
t2.join(); // 回收线程t2的资源。
cout << "aa=" << aa << endl; // 显示全局变量aa的值。
}
上述代码中,我们可以知道对于aa的自增操作是原子的,atomic还重载了--
、+=
和-=
。注意,atomic类型数据是禁用赋值函数和拷贝构造函数的。
【注】如果atomic<int> aa(0);
换成atomic<int> aa= 0;
,可能会出错。atomic<int> aa= 0;
好像是C++17才支持的。在以前的C++版本中,atomic<int> aa= 0;
中会调用赋值函数而导致错误。
操作原子变量的常用函数:
void store(const T val) noexcept; // 把val的值存入原子变量。
T load() noexcept; // 读取原子变量的值。
T fetch_add(const T val) noexcept; // 把原子变量的值与val相加,返回原值。
T fetch_sub(const T val) noexcept; // 把原子变量的值减val,返回原值。
T exchange(const T val) noexcept; // 把val的值存入原子变量,返回原值。
T compare_exchange_strong(T &expect,const T val) noexcept; // 比较原子变量的值和预期值expect,如果当两个值相等,把val存储到原子变量中,函数返回true;如果当两个值不相等,用原子变量的值更新预期值,函数返回false。CAS指令。
bool is_lock_free(); // 查询某原子类型的操作是直接用CPU指令(返回true),还是编译器内部的锁(返回false)。