本文已参与「新人创作礼」活动,一起开启掘金创作之路。
并发编程很重要,在很多场景下我们需要业务在后台持续运行,例如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;
}
可以看到返回的是 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();
}
}
}
备注:这一节就简单的对线程做个简单的小例子,接下来会继续深入下去