Cpp_Concurrency_In_Action-读书笔记(day 1)
来源:互联网 发布:prize软件破解版 编辑:程序博客网 时间:2024/06/06 00:38
2.1 线程管理的基础
每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执行完入口函数后,线程也会退出。在为一个线程创建了一个 std::thread 对象后,需要等待这个线程结束;不过,线程需要先进行启动。下面就来启动线程。
2.1.1 启动线程
第1章中,线程在 std::thread 对象创建(为线程指定任务)时启动。最简单的
情况下,任务也会很简单,通常是无参数无返回(void-returning)的函数。这种函数在其所属线程上运行,直到
函数执行完毕,线程也就结束了。在一些极端情况下,线程运行时,任务中的函数对象需要通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号,让线程停止。线程要做什么,以及什么时候启动,其实都无关紧要。总之,使用C++线程库启动线程,可以归结为构造 std::thread 对象:
void do_some_work();
std::thread my_thread(do_some_work);
为了让编译器识别 std::thread类,这个简单的例子也要包含 <thread> 头文件。如同大多数C++标准库一样, std::thread 可以用可调用(callable)类型构造,将带有函数调用符类型的实例传入 std::thread类中,替换默认的构造函数。
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
代码中,提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致,否则得到的结果会与我们的期望不同。
启动了线程,你需要明确是要等待线程结束(加入式——参见2.1.2节),还是让其自主运行(分离式——参见2.1.3节)。如果 std::thread对象销毁之前还没有做出决定,程序就会终止
( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。2.1.3节中,会介绍对应的方法来处理这两种
情况。需要注意的是,必须在 std::thread 对象销毁之前做出决定——加入或分离线程之前如果线程就已经结束,想再去分离它,线程可能会在std::thread 对象销毁之后继续运行下去。
如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周期增加了这个问题发生的几率。这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。下面的清单中就展示了这样的一种情况。
清单2.1 函数已经结束,线程依旧访问局部变量
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1. 潜在访问隐患:悬空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行
这个例子中,已经决定不等待线程结束(使用了detach()②),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量。如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;当然,这从来就不是一个好主意——这种情况发生时,错误并不明显,会使多线程更容易出错。处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁。但对于对象中包含的指针和引用还需谨慎,例如清单2.1所示。使用一个能访问局部变量的函数去创建线程是一个糟糕的主意(除非十分确定线程会在函数完成前结束)。此外,可以通过加入的方式来确保线程在函数完成前结束
2.1.2 等待线程完成
如果需要等待线程,相关的 std::thread 实例需要使用join()。清单2.1中,将my_thread.detach() 替换为 my_thread.join(),就可以确保局部变量在线程完成后,才被销毁。在这种情况下,因为原始线程在其生命周期中并没有做什么事,使得用一个独立的线
程去执行函数变得收益甚微,但在实际编程中,原始线程要么有自己的工作要做;要么会启动多个子线程来做一些有用的工作,并等待这些线程结束。join()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这
些,你需要使用其他机制来完成,比如条件变量和期待(futures),相关的讨论将会在第4章继续。调用join()的行为,还清理了线程相关的存储部分,这样std::thread 对象将不再与已经
完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(), std::thread 对象就不能再次加入了,当对其使用joinable()时,将返回否(false)。
2.1.4 后台运行线程
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread 对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,且没有任何用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应
用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,"发后即忘"(fire andforget)的任务就使用到线程的这种方式。
如2.1.2节所示,调用 std::thread成员函数detach()来分离一个线程。之后,相应的 std::thread对象就与实际执行的线程无关了,并且这个线程也无法加入:
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
为了从 std::thread 对象中分离线程(前提是有可进行分离的线程),不能对没有执行线程的 std::thread 对象使用detach(),也是join()的使用条件,并且要用同样的方式进行检查——当std::thread 对象使用t.joinable()返回的是true,就可以使用t.detach()。
- Cpp_Concurrency_In_Action-读书笔记(day 1)
- Effective Java读书笔记一(Java Tips.Day.1)
- Day 1
- day 1
- DAY 1
- day 1
- day-1
- DAY 1
- Day 1
- day(1)
- day 1
- Day 1
- Day 1
- Day-1
- DAY-1
- Day 1
- Day 1
- Day 1
- Integer, int使用陷阱
- [总结]机器学习中用到的线性代数公式,看完这个就够了
- mysql 外键的理解和作用
- 一些规范和常识
- 火绒内核注入dll方式win7-win10通用x64下不触发PG
- Cpp_Concurrency_In_Action-读书笔记(day 1)
- bzoj 4008 亚瑟王 期望概率dp
- Java面试常见算法题
- jquery ajax flask 前后端通讯
- 跨域出现的原因和解决
- Leetcode 152. Maximum Product Subarray
- java-抽象类员工案例
- 多目标跟踪 MDP Tracking 代码配置与运行
- 水果