C++并发(一)线程了解

205 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

并发编程很重要,在很多场景下我们需要业务在后台持续运行,例如Android的Service,或者是相关耗时操作,而我们的主线程不能长时间在阻塞的状态下,因此我们需要使用并发来处理一些业务流程,至于什么是并发,这里就简单介绍一点,其实就是两个任务同时运行。

1、一个简单的线程

#include <thread>

using namespace std;

void hello() {
	std::cout << "hello thread" << endl;
}
int main() {

	std::thread t(hello);
	t.join();
	return 0;
}

这里使用 join() 是为了在main线程中等待子线程执行完毕后再结束主线程

当然创建线程的方式有很多中,下面,简答的举几个例子

class Background_task {

public:
	void operator()()const {
		do_something();
		do_something_else();
	}
};
int main() {
	//Background_task background_task; 创建对象
	// 三种通过函数对象的方式来创建线程
	//std::thread thread_task(background_task);
	//std::thread thread_task( (Background_task()));
	//std::thread thread_task( (Background_task()));
	// 
	//通过lambda的方式创建线程
	std::thread thread_task([] {
		do_something();
		do_something_else();
		});
	thread_task.join();
	return 0;
}

2、线程的变量引用和join和detach()

在C++线程中,我们需要注意线程函数内部的指针引用问题,如果外部函数不等待线程结束,那么我们就必须要保证数据的有效性,这个会出现一种什么现象呢,也就是当外部函数已经执行完,指针已经释放了,但是在线程中还持有对外部函数中变量的指针引用,这个很可能就会导致线程报错

void do_something(int& j) {
	cout << "do_something" << j << endl;
}

struct MyFunc{
	int& data;
	MyFunc(int& da) :data(da) {} // 这个可以理解为构造函数
	void operator()() { //重载操作符()
		for (unsigned j = 0; j < 1000000; ++j) {
			//这里持续引用 int& data
			do_something(data);
		}
	}
};

void oops() {
	int some_local_state = 0 ;
	MyFunc func(some_local_state);
	std::thread my_thread(func);
	my_thread.detach();
}
int main() {
	oops();
	return 0;
}

那么一种比较好的方式就是将数据拷贝到线程当中去,而不是通过指针引用的方式来进行,如果需要等待线程完成就要向上面那种操作,使用 join(),但是实际开发中,一般外部线程没有那么好,可以一直等待子线程去执行,如果确实要使用join,那么要在合适的位置选择join(),并且线程中可能会产生异常. 当然我们也可以在线程运行后使用后detach()函数分离线程,这里只是做一个简单的介绍,详细的,读者可以查阅相关资料

3、特殊情况的线程等待

1、外部线程出现异常,我们需要在异常的时候去处理

void thread_join() {
	int data = 0;
	MyFunc myfunc(data);
	std::thread t(myfunc);//创建一个线程并执行,使用函数对象的方式
	try {
		do_something(data);
	}
	catch (...) {
		//当遇到异常的时候,需要join
		t.join();
		throw;
	}
	//最后正常也要执行join
	t.join();
}

2、使用等待线程 这个作用其实就是在线程准备释放资源的时候,我们使用join,如下操作

class Thread_guard {
public:
	explicit Thread_guard(std::thread &t1):t(t1) {

	};
	~Thread_guard() {
		if (t.joinable()) {
			t.join();
		}
	}
	Thread_guard(Thread_guard const&) = delete;
	Thread_guard &operator=(Thread_guard const &) =  delete;
	
private:
	std::thread& t;
};
void  working_f() {
	int data = 0;
	MyFunc func(data);
	std::thread t(func);
	Thread_guard g(t);

	
	int *data2 ;
	*data2 = 3;
	do_something(*data2);
}

working_f在执行完毕后,开始回收数据,调用Thread_guard的析构函数时候执行join 还是那句话,如果不想等待线程结束,可以调用detach分析线程

4、后台运行线程

正如上面所说,使用detach函数后,线程就与主线程脱离关系了,也就在后台运行了,如果线程分离,那么久不会有std::thread对象来引用它

void do_work_background() {
	Sleep(10000);
	cout << "-----2" << endl;
}
int main() {
	cout << "-----" << endl;
	std::thread t(do_work_background);
	t.detach();

	cout << "joinable" << t.joinable() << endl;

	assert(!t.joinable());// 非0表示true 0表示false
	return 0;
}

image.png

可以看到返回的是 0 也就是false,并且-----2没有打印出来,因为已经跟主线程解绑了

5、举例子

以C++并发编程中的例子说明多线程的问题,例如我们的多文档操作问题,用户可能在编辑A文档后,中途又去新建,或者编辑B文章,这个时候使用detach分离线程就比较实用了。 void edit_document(std::string const &filename) {

//打开并显示文件
open_doucumet_and_display_gui(filename);
//判断是否编辑完成了
while (!done_editing) {
	user_command cmd = get_user_document();
	//判断需要执行的命令
	if (cmd.type == open_new_document) {//如果是新创建文件的命令,那么就创建一个新的线程去执行
		std::string const new_name = get_filename_from_user();
		std::thread t(edit_document, new_name);
		t.detach()

	}esle{//如果不是创建一个新的线程,那就是编辑旧的文本数据信息
		process_user_input();
	}
	
}

}

备注:这一节就简单的对线程做个简单的小例子,接下来会继续深入下去