二手书的网站建设,免费发帖推广的平台,怎么在网上卖东西教程,杭州化妆品网站建设简介
本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。
教程视频链接如下#xff1a;线程基础#xff1a;视频教程
文档链接如下#xff1a;线程基础#xff1a;笔记文档
理论上直接看 up 提供的笔记文档即可#xff0c;我这里主要是记录一些我自己…简介
本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。
教程视频链接如下线程基础视频教程
文档链接如下线程基础笔记文档
理论上直接看 up 提供的笔记文档即可我这里主要是记录一些我自己觉得可能需要补充的点。
线程发起
void thread_work1(std::string str) {std::cout str is str std::endl;
}
//1 通过()初始化并启动一个线程
std::thread t1(thead_work1, hellostr);解释一下这行代码
std::thread t1(thread_work1, hellostr);1、创建 std::thread 对象t1 是一个 std::thread 类型的对象它代表了一个即将或正在执行的线程。
2、指定线程要执行的任务通过构造函数我们告诉 t1 线程应该执行哪个函数。在这个例子中任务是调用 thread_work1 函数。
3、传递参数给任务thread_work1 函数需要一个 std::string 类型的参数。在构造 t1 时我们通过传递 hellostr 变量来提供这个参数。hellostr 是一个已经定义并初始化的 std::string 对象其值将被传递给 thread_work1 函数。
4、启动线程当 std::thread 对象被构造时它会自动启动一个新线程来执行指定的任务即调用 thread_work1 函数并将 hellostr 作为参数传递。这个过程是异步的意味着主线程即创建 t1 的线程将继续执行其后续指令而不会等待新线程完成。
5、线程管理一旦线程被启动它就在自己的执行上下文中独立运行。但是std::thread 对象 t1 提供了管理这个线程的手段比如通过调用 t1.join() 来等待线程完成或者通过调用 t1.detach() 来分离线程让它在后台运行而 t1 对象不再管理它。
注意普通函数的函数名就是这个函数实际的地址。
我们可以来试验一下这个事情
#include iostream
#include string
#include thread # 注意使用多线程时要包含这个头文件using namespace std;//线程函数
void thread_work1(std::string str) {std::cout str is str std::endl;
}int main() {string hellostr hello world;//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);return 0;
}可以看见运行结果是程序崩溃了为什么呢
当我们定义完线程 t1 之后它就会自动初始化并开始运行那么在后台就会开始执行 thread_work1 这个线程函数然后输出 hello world。但是程序此时继续往下走时我们并没有将这个线程挂起或者停靠结果主线程就结束了主线程结束就必须要回收 hellostr 这个字符串资源那么就可能会存在这个资源已经被释放了那么在 thread_work1 里虽然依然能够调用资源但是有可能会出问题只是当前例子没出。
因为在代码中我们可以看到传参是通过值传递也就是把外部的局部变量作为一个拷贝拷贝给线程函数 thread_work1 所以在这个例子中字符串依然可以正常输出那为什么会崩溃就是因为主线程退出了而子线程则有可能是还在运行的那么就会崩溃。
但要注意即使我们能够人为的保证让主线程在子线程执行完之后再结束主线程比如让主线程睡上几秒上述的崩溃问题也依然存在。
这是因为11新标准的这套线程 API 做了优化当编译器发现我们启动了一个线程但却没有把这个线程做善后的工作比如join或者detach掉那么就会出现主线程在回收资源的时候就会调用这个线程的析构函数析构函数内部就会执行一个很生硬的 terminate 函数这个函数就会强制终止这个函数的强制终止是会调用 assert 断言导致崩溃的。
线程等待
因此为了防止这样的崩溃问题我们可以加入一个 join 函数
int main() {string hellostr hello world;//通过 () 初始化并启动一个线程thread t1(thread_work1, hellostr);//主线程等待子线程退出t1.join();return 0;
}这个 t1.join 会让主线程去等待子线程 t1 执行完了主线程再继续往下执行。
此时再运行将不会发生问题。
仿函数函数对象作为参数
当我们用仿函数作为参数传递给线程时也可以达到启动线程执行某种操作的含义。
但是要注意一些问题我们可以来看个例子
class background_task {
public:// 实现了括号运算符的类就称可以创建函数对象void operator()() {cout background_task called endl;}
};int main() {//t2 被当作函数对象的定义其类型为返回 thread参数为 background_taskthread t2(background_task());//t2.join(); 编译错误return 0;
}线程对象 t2 会去执行 background_task 这个类的函数对象当我们使用 background_task() 的时候会调用这个类的构造函数结果是会生成一个对象这个对象传递给了这个线程 t2 作为参数同时因为我们重载了括号运算符所以这个对象可以直接执行也就是可以把对象当成函数来使用这也是函数对象的意义。
但是可以看到在调用 join 函数时出现编译错误 这是因为编译器会将 thread t2(background_task()); 这行代码解释成了一个函数指针, 返回一个 std::thread 类型的值, 这个函数指针的参数也为一个函数指针, 这个函数指针返回值则为background_task, 参数为void。可以理解为如下
std::thread (*)(background_task (*)())这样看有点不太好懂因为我们说编译器会将 t2 当成一个函数指针不妨拆解一下上面这行代码
std::thread 是返回值(*) 是一个函数指针()是参数列表最后 background_task (*)() 是该函数指针的参数也是一个函数指针。
再对比一下函数指针的声明形式
返回类型 (*指针变量名)(参数类型列表);这样应该就好明白多了。
我们明明是想定义一个线程对象 t2却被编译器解释成了一个函数指针这肯定是不行的。
解决方案如下
//可多加一层()
//此时编译器就会认为其是一个对象进行正常的线程初始化并启动了
std::thread t2((background_task()));
t2.join();//可使用{}方式进行变量初始化
std::thread t3{ background_task() };
t3.join();此时编译就正常了。
lambda表达式
lambda 表达式也可以作为线程的参数传递给thread
std::thread t4([](std::string str) { std::cout str is str std::endl;}, hellostr);t4.join();线程detach
线程允许采用分离的方式在后台独自运行C concurrent programing书中称其为守护线程。
方式是使用 detach 函数该函数会让线程在后台运行它不会受主线程的影响主线程可以直接执行自己的任务子线程就和主线程分离了它们会各自使用自己的资源。
但是这里要注意分离的时候一旦子线程需要用到主线程的资源时由于主线程运行结束资源释放了那么子线程在获取主线程资源时就容易产生问题来看下面的例子。
struct func {//对于类和结构体的成员属性是引用的话那么可以在构造函数初始化列表中初始化int _i;func(int i) : _i(i) {}void operator()() {for (int i 0; i 3; i) {_i i;std::cout _i is _i std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}}
};void oops() {int some_local_state 0;//使用函数对象创建 func 类的对象并调用函数func myfunc(some_local_state);//创建并启动线程std::thread functhread(myfunc);//演示隐患子线程还在访问局部变量 some_local_state但局部变量可能会随着 } 结束而回收或随着主线程退出而回收functhread.detach();
}int main() {// detach 注意事项oops();//防止主线程退出过快需要停顿一下让子线程跑起来detachstd::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}主线程在执行完 oops() 后又睡了一秒然后就退出了。
虽然主线程退出了但是子线程还在执行。不过这里要注意因为主线程就是进程存在的主要形式进程包括主线程以及其衍生的一众子线程因此主线程如果结束了那么整个进程也就结束了那自然而然所有的子线程即使是在 detach 的状态也会被操作系统给全部回收掉。
上面的例子存在隐患因为some_local_state是局部变量, 当oops调用结束后局部变量some_local_state就可能被释放了而线程还在detach后台运行容易出现崩溃。
所以当我们在线程中使用局部变量的时候可以采取几个措施解决局部变量的问题
1、通过智能指针传递参数因为引用计数会随着赋值增加可保证局部变量在使用期间不被释放这也就是我们之前提到的伪闭包策略。
2、将局部变量的值作为参数传递这么做需要局部变量有拷贝复制的功能而且拷贝耗费空间和效率。
3、将线程运行的方式修改为join这样能保证局部变量被释放前线程已经运行结束。但是这么做可能会影响运行逻辑。
比如下面的修改
void use_join() {int some_local_state 0;func myfunc(some_local_state);std::thread functhread(myfunc);functhread.join();
}// join 用法
use_join();异常处理
当我们启动一个线程后如果主线程产生崩溃会导致子线程也会异常退出因为主进程就是依赖于主线程存在的也就是之前说的调用terminate。如果子线程在进行一些重要的操作比如将充值信息入库等那么丢失这些信息是很危险的。所以常用的做法是捕获异常并且在异常情况下保证子线程稳定运行结束后主线程再抛出异常结束运行。如下面的逻辑
void catch_exception() {int some_local_state 0;func myfunc(some_local_state);std::thread functhread{ myfunc };try {//本线程做一些事情,假设可能引发崩溃std::this_thread::sleep_for(std::chrono::seconds(1));}catch (std::exception e) {//一旦引发崩溃那么就会在这里被捕获住//捕获到异常后主线程不能马上崩溃要先等子线程运行结束functhread.join();//子线程运行结束后此时主线程再来处理该异常throw;}//如果没有异常那就正常继续下去即可functhread.join();
}但是用这种方式编码会显得臃肿可以采用 RAII 技术保证线程对象析构的时候等待线程运行结束回收资源。
介绍一下 RAII 技术 那么我们就来写一个简单的线程守卫
class thread_guard {
private:std::thread _t;
public:explicit thread_guard(std::thread t):_t(t){}~thread_guard() {//join只能调用一次if (_t.joinable()) {_t.join();}}thread_guard(thread_guard const) delete;thread_guard operator(thread_guard const) delete;
};可以这么使用
void auto_guard() {int some_local_state 0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);//本线程做一些事情std::cout auto guard finished std::endl;
}
auto_guard();慎用隐式转换
C中会有一些隐式转换比如 char* 转换为 string 等。这些隐式转换在线程的调用上可能会造成崩溃问题
void danger_oops(int som_param) {char buffer[1024];sprintf(buffer, %i, som_param);//在线程内部将char const* 转化为std::stringstd::thread t(print_str, 3, buffer);t.detach();std::cout danger oops finished std::endl;
}当我们定义一个线程变量thread t时传递给这个线程的参数buffer会被保存到thread的成员变量中。
而在线程对象t内部启动并运行线程时参数才会被传递给调用函数print_str。
而此时 buffer 可能随着 } 运行结束而释放了。
改进的方式很简单我们将参数传递给thread时显示转换为string就可以了这样thread内部保存的是string类型。
void safe_oops(int some_param) {char buffer[1024];sprintf(buffer, %i, some_param);std::thread t(print_str, 3, std::string(buffer));t.detach();
}关于为什么参数会像我说的这样保存和调用我在之后会按照源码给大家讲一讲。
引用参数
当线程要调用的回调函数参数为引用类型时需要将参数显示转化为引用对象传递给线程的构造函数如果采用如下调用会编译失败
void change_param(int param) {param;
}
void ref_oops(int some_param) {std::cout before change , param is some_param std::endl;//需使用引用显示转换std::thread t2(change_param, some_param);t2.join();std::cout after change , param is some_param std::endl;
}即使函数 change_param 的参数为 int 类型我们传递给 t2 的构造函数为 some_param, 也不会达到在change_param 函数内部修改关联到外部some_param的效果。因为 some_param 在传递给 thread 的构造函数后会转变为右值保存右值传递给一个左值引用会出问题所以编译出了问题。
改为如下调用就可以了
void ref_oops(int some_param) {std::cout before change , param is some_param std::endl;//需使用引用显示转换std::thread t2(change_param, std::ref(some_param));t2.join();std::cout after change , param is some_param std::endl;
}绑定类成员函数
有时候我们需要绑定一个类的成员函数
class X
{
public:void do_lengthy_work() {std::cout do_lengthy_work std::endl;}
};
void bind_class_oops() {X my_x;std::thread t(X::do_lengthy_work, my_x);t.join();
}这里大家注意一下如果thread绑定的回调函数是普通函数可以在函数前加 或者不加 因为编译器默认将普通函数名作为函数地址如下两种写法都正确
void thead_work1(std::string str) {std::cout str is str std::endl;
}
std::string hellostr hello world!;
//两种方式都正确
std::thread t1(thead_work1, hellostr);
std::thread t2(thead_work1, hellostr);但是如果是绑定类的成员函数必须添加 。
使用move操作
有时候传递给线程的参数是独占的所谓独占就是不支持拷贝赋值和构造但是我们可以通过std::move的方式将参数的所有权转移给线程如下
void deal_unique(std::unique_ptrint p) {std::cout unique ptr data is *p std::endl;(*p);std::cout after unique ptr data is *p std::endl;
}
void move_oops() {auto p std::make_uniqueint(100);std::thread t(deal_unique, std::move(p));t.join();//不能再使用p了p已经被move废弃// std::cout after unique ptr data is *p std::endl;
}