C++并发编程实战——02.线程管理

线程管理

每个程序至少有一个执行main()函数的线程,创建std::thread对象就是启动线程,之后需要等待这个线程结束。std::thread 可以通过有函数操作符类型的实例进行构造。

需要避免“最令人头痛的语法解析”(C++‘s most vexing parse)。如果你传递了一个临时变量,而不是一个命名的变量。C++编译器会将其解析为函数声明,而不是类型对象的定义。std::thread my_thread(background_task());

避免方法:

  • 使用多组括号std::thread my_thread((background_task()));
  • 使用统一的初始化语法std::thread my_thread{background_task()}
  • 使用Lambda表达式std::thread my_thread([]{do_something();});

线程基本操作

线程启动后是要等待线程结束,还是让其自主运行。当std::thread对象销毁之前还没有做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。因此,即便是有异常存在,也需要确保线程能够正确汇入join()或分离detach()

如果不等待线程汇入,就必须保证线程结束之前,访问数据的有效性。常规处理方法:将数据复制到线程中。使用访问局部变量的函数去创建线程是一个糟糕的主意。也可以通过join()来确保线程在主函数完成前结束。

当你需要对等待中的线程有更灵活的控制时(线程等待或查看是否结束)需要使用其他机制来完成,比如条件变量和future。调用join(),还可以清理线程相关的内存,这样std::thread对象将不再与已经完成的线程有任何关联。只能对一个线程用一次join()不能再次汇入join()后使用joinable()返回false

如果等待线程,则需要细心挑选使用join()的位置。当在线程运行后产生的异常,会在join()调用之前抛出,这样就会跳过join()在无异常的情况下使用join()时,需要在异常处理过程中调用join()避免生命周期问题

如果线程在函数之前结束——就要查看是否因为线程函数使用了局部变量的引用——而后再确定一下程序可能会退出的途径。解决方案:使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),提供一个类,在析构函数中使用join()

分离线程的确在后台运行,所以分离的线程不能汇入。不过C++运行库保证, 当线程退出时,相关资源的能够正确回收。

分离线程通常称为守护线程(daemon threads)。分离线程只能确定线程什么时候结束,发后即忘(fire and forget)的任务使用到就是分离线程。

不能对没有执行线程的std::thread对象使用detach()用同样的方式进行检查——当std::thread对象使用t.joinable()返回的是true,就可以使用t.detach()

传递参数

向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加 参数即可,参数会拷贝至新线程的内存空间中即使函数中的参数是指针或引用的形式

指针参数的例子:

void f(int i,std::string const& s);
void oops(int some_param){
	char buffer[1024];
	sprintf(buffer, "%i",some_param);
	std::thread t(f,3,buffer);//直接拷贝出错!拷贝操作可能在隐式类型转换之前!
    //解决方案:显式类型转换
    std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬空指针
	t.detach();
}

引用参数的例子:

void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w){
    widget_data data;
    std::thread t(update_data_for_widget,w,data);//data以右值传递,但编译出错
    //解决方案:使用std::bind
    std::thread t(update_data_for_widget,w,std::ref(data));//ref参数转为引用
    display_status();
    t.join();
    process_widget_data(data);
}

成员函数的例子:

class X{
public:
    void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);
//参数一为成员函数指针;参数二为类对象指针;参数三为成员函数参数

移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一 个对象的所有权在多个std::unique_ptr实例中传递。当原对象是临时变量时,则自动进行移动操作,但当原对象是一个命名变量,转移的时候就需要使用std::move()进行显示移动。

std::thread能占有其他资源:每个实例都负责管 理一个线程。所有权可在多个std::thread实例中转移(std::thread实例的可移动且不可复制性)。

  • 不可复制性表示在某一时间点,一个std::thread实例只能关联一个执行线程。
  • 可移动性使得开发者可 以自己决定,哪个实例拥有线程实际执行的所有权。

转移所有权

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::move()
std::thread t3;
t3=std::move(t2);
t1=std::move(t3);//重复赋值将使程序终止(调用std::terminate()是noexcept函数)

C++17标准给出一个建议,就是添加一个joining_thread的类型,这个类型与std::thread类似,不同是的添加了析构函数。委员会成员们对此并没有达成统一共识,所以这个类没有添加入 C++17标准中(C++20仍旧对这种方式进行探讨,不过名称为 std::jthread ),这个类实现起来也不是很困难。

确定线程数量

std::thread::hardware_concurrency()会返回并发线程的数量,在多核系统中返回值可以是CPU核芯的数量。

线程标识

std::thread::id可通过两种方式获取:

  • 调用std::thread对象的成员函数get_id()来直接获取。std::thread::type默认构造值,这个值表示“无线程”。
  • std::this_thread::get_id()(函数定义在<thread>头文件中)

如果两个对象的 std::thread::id 相等, 那就是同一个线程,或者都“无线程”;

如果不相等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。

标准库也提供了std::hash<std::thread::id>容器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值