前言
最近在读陈硕的moduo网络库的书,记录总结一些东西。
这章内容比较散,主要介绍多线程系统编程需要理解的一些知识点。看完的整个感受就是可以直接看最后的总结就行了,中间有些内容不是非常能理解的透彻。
基本线程原语
基本线程原语只需要三样东西:thread、mutex、condition。
- thread的创建与销毁
- mutex的创建、销毁、加锁、解锁
- condition_variable的创建、销毁、通知、广播、等待
有了这三样的东西,可以完成任何多线程编程任务。通常,不会直接使用thread和condition_variable,取而代之的是,高级的编程构建,比如线程池,比如之前提到过的CountDownLatch用于主线程和其他线程之间的同步。
C++系统库的线程安全性
这一节中的内容有些东西我还没能够完全理解,现下只记录下一些可理解的内容。以后回过头来再看看。
C++的标准库容器和std::string都不是线程安全的,只有std::allocator保证是线程安全的。一方面的原因是为了避免不必要的性能开销,另一方面的原因是单个成员函数的线程安全并不具备可组合性。
C++标准库中的绝大多数泛型算法是线程安全的,因为这些都是无状态纯函数。只要输入区间是线程安全的,那么泛型算法就是线程安全的。
Linux上的线程标识
linux上的线程标识不适合使用pthread_t,原因很多,详情见书。取而代之,使用的是gettid(2)系统调用,其优势书中也写的很清楚。
为了避免效率问题,使用__thread关键字做了缓存,避免每次获取线程id时都需要执行一次系统调用。
线程的创建与销毁的守则
线程的创建
线程的创建需要遵循以下几个原则:
- 程序库不应该在未提前告知的情况下创建自己的背景线程
- 尽量用相同的方式创建线程
- 在进入main()函数之前不应该启动线程
- 程序中线程的创建最好能在初始化阶段全部完成,不要为了每个计算任务或者每个网络连接去实时创建线程
以上四点都比较直观,也很容器理解。
针对第一点,如果程序库需要使用背景线程,那么最好让使用者在初始化库时传入线程池或者event loop对象,这样做是为了方便统筹线程的数目和用途,避免低优先级程序的任务独占某个线程。如果程序库在未告知的情况下使用了额外线程,那么会使得我们在规划线程资源的时候漏算一部分,甚至可能使得关键任务的计算资源无法达到性能指标。
针对第二点,统一的方式创建线程,可以方便的监控活动线程的数目。
针对第三点,main函数之前会完成全局对象的构造,各个编译单元之间的对象构造顺序是不确定的,有可能线程创建的时候,所使用全局对象还未构造,导致难以debug的错误
针对第四点,如果为了每个计算任务或者每个网络连接去实时创建线程,那么在负载急剧增加的的时候,可能会使得机器失去正常响应,导致我们无法探查到出了什么问题。
线程的销毁
线程正常退出的唯一方式就是自然死亡,任何从外部强行终止线程的做法和想法都是不合适的。强行终止线程,那么线程就么有机会清理资源,也没有机会释放已经持有的锁。
如果一定要强行终止计算任务,那么可以在计算期间性的检查某个全局标识。如果一定要强行终止某个耗时很长的任务,那么考虑把该任务独立出来作为一个进程,杀死一个进程比杀死一个进程中的线程要安全的多。
如果可以做到线程的创建在初始化阶段全部完成,那么线程不必要销毁,伴随进程一直运行,避免资源释放、线程对象生命期管理等各种问题。
__thread关键字
__thread变量存取效率可以去全局变量相比,__thread变量是每个线程有一份独立实体,各个线程的变量值互不干扰。
书中有句话,我现在还没办法特别能理解,先留着,之后再回顾
除了这个主要用途,它还可以修饰那些“值可能会变,带有全局性,但是又不值得用全局锁保护”的变量。
多线程与IO
多个线程同时操作同一个文件描述符(读写)会带来很多问题,所以通常我们遵循的原则是,每个文件描述符只由一个线程操作,以解决消息收发的顺序性问题。
用RAII包装文件描述符
Linux文件描述符,刚刚启动时,0是标准输入,1是标准输出,2是标准错误。之后如果新开一个文件,其描述符就都会是3,因为POSIX标准要求每次新开文件都必须使用当前最小可用的文件描述符号码。这导致稍不注意就可能造成串话,因为可能前后时刻出出现同一个文件描述符。
主要解决方法就是RAII。比如说,用Socket对象包装文件描述符即可,所有读写通过该对象来操作,析构函数关闭文件描述符。即Socket对象村花,那么文件描述符则存在。
多线程与fork()
多线程程序一般不允许调用fork(),因为Linux的fork()只会克隆当前线程。所以...很危险
总结
- 线程数目的规划和CPU数目有关,和负载无关。
- 一个程序最好在一开始创建好所有需要的线程,并且反复使用,避免反复创建、销毁。
- 线程规划上,每个线程应该明确各自的职责,管IO的管IO,管计算的搞个线程池
- 线程之间的交互尽量简洁,最好只用消息传递(如BlockingQueue)。如果必须用锁,那么最好避免一个线程同时持有多把锁,以彻底防止死锁
- 预先考虑清楚被暴露给其他线程的对象的读写状况
没有帐号?立即注册