muduo网络库:11—C++多线程编程精要之(基本线程原语的选用、Linux上的线程标识、善用__thread关键字、Linux新增系统调用的启示)

一、基本线程原语的选用

  • 我认为用C/C++编写跨平台(只针对POSIX操作系统)的多线程程序不是普遍的需求,因此我们只谈现代Linux(2004年Linux 2.6内核发布之后,NPTL线程库)下的多线程编程
  • POSIX threads的函数有110多个, 真正常用的不过十几个​。而且在C++程序中通常会有更为易用的wrapper,不会直接调用系统运维工程师Pt系统运维主要做什么hreads函数。这11个最基本的Pthreads函数是:
  • 2个:​线程的创建和等待结束(join)。封装为muduo::Thread
  • 4个:​mutex的创建、销毁、加锁、解锁。封装为 muduo:linux必学的60个命令:M封装测试utexLock
  • 5个:​条件变量的创建、销毁linux常用命令、等待、通知、广播。封装为 muduo:linux操作系统基础知识:Condition
  • 这些封装class都很直截了当,加起来也就一两百行代码,却已经构成了多线程编程的全部必备原语。用这三样东西(thread、mutex、 condition)可以完成任何多线程编程任务。当然我们一般也不会直接使用它们(mutex除外),而是使用更高层的封装,例如mutex::T封装hreadPool和mutex::CountDownLatch等(见前面“线程同步精要”文章)
  • 除此之外,​Pthreads还提供了其他一些原语,有些是可以酌情使用的,有些则是不推荐使用的:
  • 可以酌情使用的有:
  • pthread_once,封装为mudu封装是什么o::Singleton。其实不如直接用全局 变量
  • pthread_key*,封装为muduo::ThreadLocal。可以考虑用 __thread替换之
  • 不建议使用:
  • pthread_rwlock,读写锁通常应慎用。muduo没有封装读写锁,这是有意的
  • sem_*,避免用信号量(semaphore)。它的功能与系统调用失败是怎么回事条件变量重合,但容易用错
  • pthread_{cancel, kill}。程序中出现了它们,则通常意味着设计出了问题
  • 不推荐使用读写锁的原因是:​它往往造成提高性能系统调用失败是怎么回事的错觉(允许多个线程并发读),实际上在很多情况下,与使用最简单的mutex相比,它实际上降低了性能。另外,写操作会阻塞读操作,如果要求优化读操作的延迟,用读写锁是不合适的
  • 多线程系统编程的难点不在于学习线程原语(pr系统运维工作内容imitives),而在于理解多线程与现有的C/C++库函数和系统调用的交互关系,系统调用是什么以进一步学习如何设计并实现线程安全且高效的程序

二、Linux上的线程标识

  • 关于线程标识,还可以参阅​​​​

pthread_t

  • POSIX threads库​提供了pthread_系统运维工作内容self函数用于返回当前进程linux系统安装的标识符​,其​类型为pthread_t
  • pthread_t​不一定是一个数值类型(整数或指针),也有可能是一个结构体,​因此Pthreads专门提供了pthread_e系统调用是由操作系统提供给用户qual函数用于对比两个线程标识符是否相等系统调用和函数调用的区别
  • 这就带来一系列问题,包括:
  • 无法打印输出pthread_t,​因为不知道其确切类型。也就没法在日志中用它示当前线程的id
  • 无法比较pt封装测试hread_t的系统调用和函数调用的区别大小或计算其hash值,​因此无法用作关联系统调用容器的key
  • 无法定义一个非封装系统系统调用的目的是什么的pthread_t值,​用来示绝对不可能存在的线程id,因此MutexLlinux操作系统基础知识ock class没有办法有效判断当前线程是否已经持有本锁
  • pthread_t值只在进程内有意义,​与操作系统的任务调度之封装光刻机间无法建立有效关封装测试联。比方说在/proc文件系统中找不到pthread_t对应的task
  • 另外,g封装是什么libc的Pthreads实现实际上把pthread_t用作一个结构体指针 (它的类型是unsigned long),指向一块动态分配的内存,而且这块内存是反复使用的。封装继承多态这就造成pthread_t的值很容易重复​。Pthreads​只保证​同一进程之内,同一时刻的各个线程的id不同;​不能保证​同一进程先后多个线程具有不同的id,更不要说一台机器上多个进程之间的id唯一性了
  • 例如,​下面这段代码中先后封装测试两个线程的标识符是相同的:
#include <stdio.h>
#include <pthread.h>

void *threadFunc(void*){}

int main()
{
pthread_t t1,t2;

pthread_create(&t1,NULL,threadFunc,NULL); //创建线程
printf("%lx\n",t1); //打印线程id
pthread_join(t1,NULL); //阻塞等待t1线程结束

pthread_create(&t2,NULL,threadFunc,NULL);
printf("%lx\n",t2);
pthread_join(t2,NULL);

return 0;
}
  • 一次运行结果如下系统调用是什么
  • 因此,pthread_t并不适合用作程序中对线程的标识符

pid_t

  • 在Linuxlinux重启命令上,我建议​使用gettid系统调用的返回值作为线程ilinux操作系统基础知识d
  • 这么做linux的好处有:
  • 它的类型是pid_t,其值通常是一个小整数​(最大值是/proc/s封装系统ys/kernel/pid_max,默认值是32768),便于在日志中输linux常用命令
  • 在现代Linux中,它​直接示内核的任务调度id,因此在/proc文件系统中​可以轻易找到对应项:/proc/tid或/prod/pid/task/tid
  • 在其他系统工具中也容易定位到具体某一个线程,​例如在top中我们可以系统运维是干嘛的按线程列出任务,然后找出CPU使用率最高的线程id,再根据程序日志判断到底哪一个线程在耗用CPU
  • 任何时linux刻都是全局唯一的封装​并且由于Linux分配新pid采用递增轮回办法,短时间内启动的多个线程也会具有不同的线程id
  • 0是非法值,​因为操作系统第一个进程init的pid是1
  • 但是glibc并没有封装这个系统调用,需封装基板要我们自己实现​。封装gettid很简单,但是每次都执系统调用失败是怎么回事行一次系统调用似乎有些浪费,封装如何才 能做到更高效呢?
  • muduo::CurrentThread::tid()采取的办法是:
  • 用​__thread变量来缓存gettid的返回值​,这样只有在本线程第一次调用的时候才进行系统调用,以后都是直接从th系统调用失败read local缓存的线程id拿到结果(这个做法是受了glibc封装getpid()的启发),效率linux常用命令无忧
  • 多线程程序在打日志的时候​可以在每一条日志消息中包含当前线程的id,不必担心有效率损失​。读者有兴趣的话封装基板可以对比一下boost::this_thread::get_id()的实现效率
  • 还有一个小问题,封装性万一程序执行了fork,,那么系统运维是干嘛的子进程会不会看到stallinux常用命令e的系统运维工程师缓存结果呢?解决办法是​用pthread_atfork()注册一个回linux删除文件命令调,用于清空缓存的线程id​。具体代码见muduo/base/CurrentThread.h和Thread.cc

三、善用__thread关键字

  • __thread关键字概述:
  • __thread​是GCC内置的封装性线程局部存储设施系统调用是由操作系统提供给的(thread local storage)
  • 它的实现非常高系统调用是由操作系统提供给的效,比pthread_key_t快很多​,linux系统见Ulrich Drepper写的 《ELF Handling For Thread-Local Storage》(​​http://www.akkadia.org/drepper/tls.pdf​​)
  • 系统运维是干嘛的__thread变量的存取效率可与全局变量相比:


                                            muduo网络库:11---C++多线程编程精要之(基本线程原语的选用、Linux上的线程标识、善用__thread关键字、Linux新增系统调用的启示)

_thread使用规则

  • 只能用于修饰POD类型,不能修饰class类型​, 因为无法自动调用构造函数和析构函数
  • __thread​可以用于修饰全局变量、函数内的静态变量​,但是​不能用于修饰函数的局部变量或者class的普系统调用是由操作系统提供给的通成员变量
  • 另外,​__thread变量的初始化只能用编译期常量系统调用失败是怎么回事
  • 例如:


                                            muduo网络库:11---C++多线程编程精要之(基本线程原语的选用、Linux上的线程标识、善用__thread关键字、Linux新增系统调用的启示)

_thread的用途

  • __thread变量​是每个线程有一份独立实体,各个线程的变量值互不干扰​。除了这个主要用途,它​还可以修饰系统调用失败是怎么回事那些“值可能会变,带有全局性,但是又不值得用全局锁保护”的变量
  • mud封装uo代码中用到了好几处 __thread,简单列举如下:
  • m封装继承多态uduo/base/Logging.cc:缓存最近一条日志时间的年月日时分秒, 如果一秒之内输出封装测试多条日志,封装继承多态可避免重复格式化。另外, muduo::strerror_tl把strerro封装测试r_r(3)做成如同strerror(3)一样好用,而且是线 程安全的
  • muduo/base/ProcessInfo.cc:用线程局部变量来简化::scandir(3)的使用
  • muduo/base/Thread.封装光刻机cc:缓存每个线程的id
  • muduo/base/EventLoop.cc:用于判断当前线程是否只有一个EventLoop 对象
  • 以上例子都linux删除文件命令是__thread修饰POD类型的变量
  • 如果要用到thre系统运维是干嘛的ad local的cl封装是什么ass对象:
  • 可以考虑使用 muduo::ThrlinuxeadLocal<T>和muduo::ThreadLocalSingleton<T>这两个class, 它能在线程退出时销毁class对象
  • 例如用examples/asio/chat/server_threaded_highperformance.cc用ThreadLocalSingleton来保存每个EventLoop线程所管辖的客户连接,以实现高效的消息转发(可以参阅后面“muduo编程示例之“串并转换”连接服务器机器自动化测系统运维是干嘛的试”文章)

四、Linux新增系统调用的启示

  • 本节的内容还可以参阅陈硕的一篇同名博客省略了signalfd、timerfd、 eventfd等linux系统内容,对此感兴趣的读者可阅读原文
  • 大致从Linux内核2.6.27起,凡是会创建文件描述符的syscall一般都增加了额外的flags参数​,可以直接指定O_NONBLOCK和 FD_CLOEXEC,例如:
  • accept4 - 2.6.28
  • eventfd2 - 2.6.27
  • in封装是什么otify_init1 - 2.6.27
  • pipe2 - 2.6.27
  • signalfd4 - 2.6.27
  • timerfd_create - 2.6.25
  • 以上6个syscall,除了最后一个是2.6.25的新功能,其余的都是增强原有的调用,把数字尾号去掉就是原来的系统调用的执行过程syscall

O_NONBLOCK

  • O_NONBLOCK的功能​是开启“非阻塞IO”封装是什么,而文件描述符默认是阻塞的
  • 这些创建文系统调用是什么件描述符的系统调用能直接设定O_NONBLOCK选项,其或许能反映当前Linux(服务端)开发的风向,即在前面“多线程服务器的编程模型”文章里推荐的​one loop per thread +linux常用命令(non-blocking IO with IO multiplexing)
  • 从这些内核改动linux命令来看,non-blocking IO已经主流到​系统运维工作内容内核增加syscall以系统调用节省一 次fcntl调用的程度了

FD_CLOEXEClinux必学的60个命令

  • 另外,​以下新系统调用可以在创建文件描述符时开启FD_CLOEXEC选项:
  • dup3 - 2.6.27
  • epoll_create1 - 2.6.27
  • socket - 2.6.27
  • FD系统调用的执行过程_CLOEXEC的功能​是让程序exec()时,进程会自动关闭这个文件描述符。​而文件描述默认是被子进程继承的(这是传统Unix的一种典型IPC,比如用pipe在父子进程间单向通信封装英文
  • 以上8个新syscall都允许直接指定FD_CLOEXEC​,或许说明fork()的主要目的已经​不再是创建work封装测试er pro系统调用cess封装是什么并通过共享的文件描述符和父进程保持通信,而是像Windows的CreateProcess那样创linux常用命令建“干净”的进程封装系统​ (fork()之后立刻exec()),其与父进程没有多少瓜葛
  • 为了回避 fo系统运维是干嘛的rk()+exec()之间文件描述符泄漏的race condition,这才在几乎所有能新建文件描述符的系统调用上引入了FD_CLOEXEC参数,参见Ulrich Drepper的短文《Secure File Descrip封装tor Handling》(​​http://udrepper.livejoumal.com/20407.html​​)
  • 通过以上两个flags看来:
  • 说明Linux服务器开发的​主流模型正在由fork()+worker processes模型转变为前面文章推荐的多线程模型
  • fork()的使用频度会大大降低,将来或许只有专门负责启linux系统动别系统运维主要做什么的进程的“看门狗程 序”才会调用fork()​,而​一般的网络服务器程序不会再fork()出子进程了封装形式
  • 原因之一是,fork()一般不能在多线程程序中系统调用的目的是什么调用(参阅后面的“多线程与fork()”文章)

五、总结

  • 本专题未完结​​​​