2024.3.29记——C++多线程系列文章(三)线程归属权转移及线程识别

本文详细解释了C++中的std::thread对象如何管理线程所有权,包括通过移动语义进行线程转移,并讨论了线程ID的使用和线程间共享数据的并发操作同步问题。后续将探讨带有返回值的线程和同步机制的解决方案。
摘要由CSDN通过智能技术生成

前面文章

2024.3.27记——C++多线程系列文章(一)
2024.3.28记——C++多线程系列文章(二)之向线程函数传递参数

归属权转移

每个std::thread对象都负责管控一个执行线程。该对象只能够移动却不能复制,故线程的归属权可以在实例之间转移(切忌复制)。这就保证了,对于任一特定的执行线程,任何时候都只有唯一的std:::thread对象与之关联,还准许程序员在其对象之间转移线程归属权。

假设我们要编写函数,功能是创建线程,并置于后台运行,但该函数本身不等待线程结束,而是将其归属权向上移交给函数的调用者;或相反地,我们想创建线程,遂将其归属权传入某个函数,由它负责等待该线程结束。两种操作都需要转移线程的归属权。两种操作都需要转移线程的归属权。这正是std::thread支持移动语义的缘由。

以下例子创建2个执行线程和3个std::thread实例t1、t2、t3,并将线程归属权在实例之间多次转移。

void some_function();
void some_other_function();
std::thread t1(some_function);---  ①
std::thread t2=std::move(t1);---  ②
t1=std::thread(some_other_function);---  ③
std::thread t3;---  ④
t3=std::move(t2);---  ⑤
t1=std::move(t3);---  ⑥该赋值操作会终止整个程序

首先,我们启动新线程①,并使之关联t1。

接着,构建t2,在其初始化过程中调用std::move(),将新线程的归属权显式地转移给t2②。在②之前,t1关联着执行线程,some_function()函数在其上运行;及至②处,新线程关联的变换为t2。

然后,启动另一新线程③,它与一个std::thread类型的临时对象关联。新线程的归属权随即转移给t1。这里无须显式调用std::move(),因为新线程本来就由临时变量持有,而源自临时变量的移动操作会自动地隐式进行。

t3按默认方式构造④,换言之,在创建时,它并未关联任何执行线程。

在⑤处,t2原本关联的线程的归属权会转移给t3,而t2是具名变量,故需再次显式调用std::move(),先将其转换为右值。经过这些转移,t1与运行some_other_function()的线程关联,t2没有关联线程,而t3与运行some_function()的线程关联。

在最后一次转移中⑥,运行some_function()的线程的归属权转移到t1,该线程最初由t1启动。但在转移之时,t1已经关联运行some_other_function()的线程。因此std::terminate()会被调用,终止整个程序。该调用在std::thread的析构函数中发生,目的是保持一致性。在std::thread对象析构前,我们必须明确:是等待线程完成还是要与之分离。不然,便会导致关联的线程终结。赋值操作也有类似的原则:只要std::thread对象正管控着一个线程,就不能简单地向它赋新值,否则该线程会因此被遗弃。

std::thread支持移动操作的意义是,函数可以便捷地转移线程的归属权,示例代码如下所示。

// 从函数内部返回std::thread对象
std::thread f() {
    void some_function();
    return std::thread(some_function);
}
// 从函数内部返回std::thread对象
std::thread g() {
    void some_other_function(int);
    std::thread t(some_other_function,42);
    return t;
}
// 将thread对象作为参数传入到函数中去。右值
void h(std::thread t);
void p()
{
    void some_function();
    h(std::thread(some_function)); // 临时变量隐式转换成右值
    std::thread t(some_function);  
    h(std::move(t)); // std::move将左值转成右值
}

以下示例生成多个线程,并等待它们完成运行,这初步形成多线程完成工作的例子。

void do_work(unsigned id);
void f()
{
    std::vector<std::thread> threads;
    for(unsigned i=0;i<20;++i)
    {
        threads.emplace_back(do_work,i);  // 生成线程
    }
    for(auto& entry: threads)    // 依次在各线程上调用join()函数
        entry.join();
}

线程识别

前面文章中已经使用了线程ID概念,这里再多说一句。

线程ID所属型别是std::thread::id,它有两种获取方法。

  1. 在与线程关联的std::thread对象上调用成员函数get_id(),即可得到该线程的ID。
  2. 当前线程的ID可以通过调用std::this_thread::get_id()获得,函数定义位于头文件内。

如果std::thread对象没有关联任何执行线程,调用get_id()则会返回一个std::thread::id对象,它按默认构造方式生成,表示“线程不存在”。

std::thread::id型别的对象作为线程ID,可随意进行复制操作或比较运算;否则,它们就没有什么大的用处。如果两个std::thread::id型别的对象相等,则它们表示相同的线程,或者它们的值都表示“线程不存在”;如果不相等,它们就表示不同的线程,或者当中一个表示某个线程,而另一个表示“线程不存在”。

#include <iostream>
#include <thread>
void print(int n) {
  for (int i = 0; i < n; ++i) {
    std::cout << i << " ";
  }
  std::cout << std::endl;
  // 函数内部调用get_id,并打印线程
  std::cout << "print id:" << std::this_thread::get_id() << std::endl;
}
int main() {
  std::thread t;
  // t未指定任何线程前调用get_id
  std::cout << "default thread id:" << t.get_id() << std::endl;
  t = std::thread(print, 5);
  // join前调用get_id
  std::cout << "main before join t thread id:" << t.get_id() << std::endl;
  t.join();
  // join后调用get_id
  std::cout << "main after join t thread id:" << t.get_id() << std::endl;
  // main线程调用get_id
  std::cout << "main thread id:" << std::this_thread::get_id() << std::endl;
}

运行结果如下:

default thread id:thread::id of a non-executing thread
main before join t thread id:140395914720832
0 1 2 3 4 
print id:140395914720832
main after join t thread id:thread::id of a non-executing thread
main thread id:140395921580992

C++标准库容许我们随意判断两个线程ID是否相同,没有任何限制;

std::thread::id型别具备全套完整的比较运算符,比较运算符就所有不相等的值确立了全序(total order)关系。这使得它们可以用作关联容器的键值,或用于排序,或只要我们认为合适(从程序员的视角评判),它们还能参与任何用途的比较运算。

就所有不相等的std::thread::id的值,比较运算符确立了全序关系,它们的行为与我们的预期相符:若a<b且b<c,则有a<c,以此类推。

标准库的hash模板能够具体化成std::hashstd::thread::id,因此std::thread::id的值也可以用作新标准的无序关联容器(unordered_set/unordered_map)的键值。

std::thread::id实例常用于识别线程,以判断它是否需要执行某项操作

后续

我们前面学习了如何开辟多个线程以及线程归属权如何管理,我们基本上(暂且这样认为)可以实现将某个任务分成多个子任务,然后将每个子任务分别在不同线程上运行。最后进行汇总工作。如果每个子任务有独立的变量,也就是每个任务之间不存在共享数据,我们先前学习的基本上可以完成该任务,但是对于有共享数据的情况,可能还是鞭长莫及。这是由于如果对于同一个数据而言,如果一个线程在写的同时,如果另一个线程在读,可能会出现意想不到的现象。互斥可以解决该问题。另外我们只介绍了无返回值的线程启动函数,如果有返回值我们该怎么办?后续系列文章将逐一解答。

  1. 线程间共享数据
  2. 并发操作的同步
  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值