C++并发编程实战-第二章

第二章 管理线程

线程管理基础

启动线程

线程在构造 std: :thread对象时启动,这个对象指定了要运行的任务
    std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。
    第一参数的类型并不是c语言中的函数指针(c语言传递函数都是使用函数指针),在c++11中,增加了可调用对象(Callable Objects)的概念,总的来说,可调用对象可以是以下几种情况:
      函数指针
      重载了operator()运算符的类对象,即仿函数
      lambda表达式(匿名函数)
      std::function

  • 函数指针
// 普通函数 无参
void function_1() {
}

// 普通函数 1个参数
void function_2(int i) {
}

// 普通函数 2个参数
void function_3(int i, std::string m) {
}

std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");

t1.join();
t2.join();
t3.join();

注意:如果将重载的函数作为线程的入口函数,会发生编译错误!编译器搞不清楚是哪个函数

  • 重载了operator()运算符的类对象,即仿函数
#include <iostream>
#include <thread>
using namespace std;
class obj
{
public:
	void operator()()//暂时不带参数   线程入口点
	{
		cout << "线程开始执行" << endl;
		//...
		cout << "线程执行结束1" << endl;
	}
};
int main()
{
	obj ob;
	thread mytobj(ob);
	mytobj.join();
	cout << "hhh" << endl;
	return 0;
}

//类内声明了变量的情况
class obj
{
public:
	int &m_i1;//&m_i1,&m用了&,detach程序会出问题,去掉引用才可以detach
	obj(int &m) :m_i1(m) {}
	void operator()()//暂时不带参数   线程入口点
	{
		cout << "线程开始执行" << endl;
		//...
		//m是主函数的局部变量的引用,当主线程执行完毕之后,局部变量被释放,此时访问m_i1,结果不可预料
		cout << "m_i1  1"<<m_i1 << endl;
		cout << "m_i1  2" << m_i1 << endl;
		cout << "m_i1  3" << m_i1 << endl;
		cout << "m_i1  4" << m_i1 << endl;
		cout << "m_i1  5" << m_i1 << endl;
		cout << "m_i1  6" << m_i1 << endl;
	}
};
int main()
{
	int m = 2;
	obj ob(m);
	//主线程运行结束后,m被释放,会产生不可预料后果,那对象ob也被释放了啊?子线程为什么还存在?
	//原因:对象ob是被 复制 到线程中去的,主线程中的ob被释放,但是线程中的obj对象还存在。但是如果这个对象使用了指针引用之类的,依旧会存在问题
	//可以自己写一个拷贝构造函数证明,此处复制
	thread mytobj(ob);
	//thread mytobj1(obj());//错误,如果传递了一个临时变量,而非命名变量,编译器会将其解析为函数声明,而不是对象的定义。
	//解决办法:
	thread mytobj1((obj()));
	thread mytobj2{obj()};

	mytobj.detach();
	return 0;
}

注意:如果传递了一个临时变量,而非命名变量,编译器会将其解析为函数声明,而不是对象的定义。解决办法就是在临时变量外包一层小括号(),或者在调用std::thread的构造函数时使用{}
如果重载的operator()运算符有参数,就不会发生上面的错误。

  • lambda表达式(匿名函数)
int main()
{
	auto lambdaThread = [] {
		cout << "我的线程开始执行了" << endl;
		//-------------
		//-------------
		cout << "我的线程开始执行了" << endl;
	};
	thread myThread(lambdaThread);
	myThread.join();
	cout<<"hhh"<<endl;
	return 0;
}
  • std::function
    C++11中新引入的模板类。类模板std::function是一种通用的多态函数包装器。std::function可以存储,复制和调用任何Callable 目标的实例,例如函数,lambda表达式,绑定表达式或其他函数对象,以及指向成员函数和指向数据成员的指针。
    个人理解,fonction的作用就是把可调用对象转为统一形式,便于管理
class A{
public:
    void func1(){
    }

    void func2(int i){
    }
    void func3(int i, int j){
    }
};

A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);

std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);

bind函数相关

等待线程完成

  如果需要等待线程完成,可以调用std::thread实例的join()函数。在实际编程中,原始线程要么有自己的工作要做;要么会启动多个线程做一些有用的工作,并等待这些线程结束。
  join()是简单粗暴的技术一一要么等待线程完成,要么不等待。如果需要更细粒度的控制等待线程的过程,比如,检查某个线程是否结束,或者只等待一段时间。则要使用其他机制来完成,比如条件变量和期待(futures)。调用join()的动作,还清理了线程相关的存储,这样std::thread对象将不再与已经结束的线程有任何关联。这意味着,对一个线程只能调用一次 join();一旦已经调用过join(),std :: thread对象就不能再次连接,同时joinable()将返回false

异常场景下的等待

  如果打算等待线程,则需要细心挑选调用join()代码的位置。因为如果在线程启动后调用join()前有异常抛出,join()调用很容易被跳过。
  解决方式一:为了避免应用因为异常抛出而终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用join()时,你也需要在异常处理过程中调用join(),从而避免意外的生命周期的问题。即在catch模块中throw前join()一下;确保程序结束前jion()过。
  解决方式二:使用标准的“资源获取即初始化”(RAII,Resource Acquisition IsInitialization),并且提供一个类,在析构函数中执行 join(),如同下面清单中的代码。

//使用RAII等待线程完成
class thread_guard
{
	std::thread& t;
public:
	explicit thread_guard(std::thread& t_):t(t_)
	~thread_guard(){
		if(t.joinable()){
			t.join();}
	}
	thread_guard(thread_guard const&)=delete;//禁用默认拷贝构造函数
	thread_guard& operator=(thread_guard const&)=delete;//禁用默认赋值函数
};
struct func;//一个函数,书本清单2.1
void f()
{
	int some_local_state=0;
	func my_func(some_local_state);std: :thread t(my_func);
	thread_guard g(t);
	do_something_in_current_thread();
}

&emsps; 拷贝构造函数和拷贝赋值操作被标记为=delete,是为了不让编译器自动生成它们。拷贝或者赋值这类对象是危险的,因为它可能超过它所连接线程的作用域而继续存在。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

在后台运行线程

  调用detach()会让线程在后台运行,这就意味没有直接的方法和他通信。也就没法等待线程结束;如果线程被分离了,那就不可能有std::thread对象能引用它,所以也不能被连接。分离的线程的确在后台运行;所有权和控制会传递给C++运行时库,它会保证和线程相关的资源在线程退出的时候被正确的回收。
分离线程经常叫做守护线程(daemon threads)这是参照UNIX中守护进程的概念,这种运行在后台的进程没有任何显式的用户接口。这种线程的特点是长时间运行;它们运行在应用的整个生命周期中,可能会在后台监视文件系统,还有可能清理没用的对象缓存,亦或优化数据结构。在另一个极端,分离线程也有用武之地,比如用在有另一种机制标识线程什么时候完成的地方,或者用于发后即忘(fire and forget)的任务。
  例子:考虑一个应用程序比如文字处理器能同时编辑多个文档。在UI和内部实现有很多种处理方法。目前普遍的一种方式是用多个独立的顶层窗口,每个文档在编辑的时候对应一个。尽管这些窗口看起来完全独立,每个有它自己的菜单,但他们运行在应用的同一个实例。一种内部处理方式是,让每个文档处理窗口拥有自己的线程;每个线程运行同样的代码,但被编辑的文档有不同的数据,以及配套的窗口属性。打开一个文档就要启动一个新线程。处理请求的线程没有兴趣等待其它线程结束,因为它工作在跟别人无关的文档上。因此,这使得运行分离的线程变成首选。

//使用分离线程去处理其他文档
void edit_document(std: : string const& filename)
{
	open_document_and_display_gui(filename);
	while( !done_editing())
	{
		user_command cmd=get_user_input();
		if(cmd.type==open_new_document)
		{
			std::string const new_name=get_filename_from_user();
			std::thread t(edit_document, new_name);// 1
			t.detach();// 2
		}
		else
		{
			process_user_input( cmd);
		}
	}
}

传递参数给线程函数

向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加参数即可。但需要注意的是,在默认情况下,这些参数会被拷贝至新线程的内部存储,新创建的执行线程可以访问它们,然后像临时值那样以右值引用传递给调用对象或者函数。就算函数的参数期待一个引用时仍然是这样。

#include <iostream>
#include <thread>
#include <string>

// 仿函数
class Fctor {
public:
    // 具有一个参数 是引用
    void operator() (std::string& msg) {
        msg = "wolrd";
    }
};
int main() {
    Fctor f;
    std::string m = "hello";
    std::thread t1(f, m);

    t1.join();
    std::cout << m << std::endl
    return 0;
}
// vs下: 最终是:"hello"
// g++编译器: 编译报错

  子线程并没有成功改变外面的变量m.
  如果可以想真正传引用,可以在调用线程类构造函数的时候,用std::ref()包装一下。std::thread t1(f, std::ref(m));  后果:多个线程同时修改同一个变量,会发生数据竞争。
  同理,构造函数的第一个参数是可调用对象,默认情况下其实传递的还是一个副本。

#include <iostream>
#include <thread>
#include <string>

class A {
public:
    void f(int x, char c) {}
    int g(double x) {return 0;}
    int operator()(int N) {return 0;}
};

void foo(int x) {}

int main() {
    A a;
    std::thread t1(a, 6); // 1. 调用的是 copy_of_a()
    std::thread t2(std::ref(a), 6); // 2. a()
    std::thread t3(A(), 6); // 3. 调用的是 临时对象 temp_a()
    std::thread t4(&A::f, a, 8, 'w'); // 4. 调用的是 copy_of_a.f()
    std::thread t5(&A::f, &a, 8, 'w'); //5.  调用的是 a.f()
    std::thread t6(std::move(a), 6); // 6. 调用的是 a.f(), a不能够再被使用了
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    t6.join();
    return 0;
}

对于线程t1来说,内部调用的线程函数其实是一个副本,所以如果在函数内部修改了类成员,并不会影响到外面的对象。只有传递引用的时候才会修改。所以在这个时候就必须想清楚,到底是传值还是传引用!

转移线程所有权

  线程对象只能移动不可复制
  线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移。

void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; // 编译错误
std::thread t2 = std::move(t1); //只能移动 t1内部已经没有线程了
t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作
std::thread t3;
t3 = std::move(t2); // t2内部已经没有线程了
t1 = std::move(t3); // 程序将会终止,因为t1内部已经有一个线程在管理了

运行时选择线程数量

  C++标准库中的std::thread : : hardware_concurrency()函数会返回一个数量指示,表明执行程序真正可以并发的线程数。在一个多核系统中,返回值可以是CPU的核数。返回值也仅仅是个提示,当系统信息无法获取时,函数也可能返回0,但是,在线程间划分任务的时候它是个非常有用的指南。

标志线程

  线程标识类型为std::thread::id,并可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回默认构造的std:: thread::id对象,表示“没有任何线程”(“not any thread”)。第二种,调用std::this_thread:: get_id()也可以获得当前线程的标识,这个函数也定义在头文件中。
  std : : thread::id类型的对象可以随意的拷贝和比较,否则的话作为标识作用就不大。如果两个std: : thread::id类型的对象相等,那它们代表了同一个线程,或者都持有“没有任何线程”(not any thread)的值。如果不相等,那么就代表了两个不同线程,或者一个代表了某个线程,另一个持有“没有任何线程”的值。
参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值