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模板类(结构体),用于支持原子类型,模板参数可以是bool、char、int、long、long long、指针类型(不支持浮点类型和自定义数据类型)。
原子操作由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)。
posted @ 2023-03-30 11:19  好人~  阅读(238)  评论(0编辑  收藏  举报