进程与线程
线程特点是共享地址空间,从而可以高效地共享数据。
单线程服务程序的编程模型 和 多线程服务程序的编程模型
-
单线程服务程序的编程模型
while(!done) { int timeout_ms = max(1000, getNextTimedCallback()); int retval = ::poll(fds, nfds, timeout_ms); if (retval < 0) { // 处理错误 } else { // 处理到期的timers,回调用户的timer handler if (retval > 0) { // 处理IO事件,回调用户的IO event handler } } }
-
多线程服务程序的编程模型
- 每个请求创建一个线程
- 使用非阻塞IO + IO 复用
- one loop per thread
在这种模式下,程序里的每个IO线程都有一个Event loop。Event loop 代表了线程的主循环,需要哪个线程干活,就把 timer 或 IO channel 注册到哪个线程里的loop即可。 - 线程池
对于还有计算任务的服务程序来说,最好是使用blockingqueue
实现的任务队列。
- one loop per thread
-
单线程服务程序的优缺点
- 单线程程序的优点:开发简单
- 单线程程序的缺点:容易造成优先级反转。
假如事件a的优先级高于事件b,处理事件a需要1ms,处理事件b需要10ms,如果事件b稍早于事件a发生,那么事件a则需要等待事件10ms才能被处理。
-
适合用多线程的特点有:
- 有多个CPU可用,单核使用多线程没有意义
- 线程间有共享数据
- 提供非均质的服务
事件的响应有优先级差异,可以使用专门的线程来处理优先级高的事件 - 利用异步操作
比如logging,无论是往磁盘写log file,还是往log server发送消息都不应该阻塞 - 能扩容
一个好的多线程程序应该能享受增加CPU数目带来的好处。
-
一个多线程服务程序中的线程可以分为3类:
- IO线程
- 计算线程
- 第三方库使用的线程,比如 log 和 database 连接线程
多线程能提高并发度吗?
假如单纯采用 每个连接一个线程的 处理模型,那么并发连接数是极其有限的,Linux能同时创建的线程数量有限制。
但如果采用 one loop per thread,那么能轻轻松松处理很多个并发长连接。
多线程能提高吞吐量吗?
能
多线程能降低响应时间吗?
能
多线程程序 如何让IO和计算相互重叠,降低latency
基本思路是,把IO操作通过blockingqueue
交给别的线程去做。比如:logging
什么是线程池大小阻抗匹配原则
如果池中线程在执行任务时,密集计算所占的时间比重为P
,而系统一共有C个CPU,为了让这个CPU跑满而又不过载,线程池大小的经验公式是:
假设 C = 8,P = 1.0,线程池的任务完全是密集计算,那么 T = 8,只要8个活动线程就能让8个CPU饱和,再多也没用,因为CPU资源已经耗光了。假设 C = 8,P = 0.5,线程池的任务有一半是计算,有一半在等待IO上,那么 T = 16,考虑OS调度的灵活性,16个50%繁忙的线程能让CPU跑满,启动更多的线程并不能提高吞吐量。
基本线程原语选择
posix threads 的函数有110个,真正常用的不过十几个。主要常用的有:
- 2个:线程创建和等待结束
- 4个:
std::mutex
的创建,销毁,加锁,解锁 - 5个:条件变量的创建,销毁,等待,通知,广播
利用其上封装为count_down_latch
,多多利用高级的并发构件。
避免使用信号量,它的功能和条件变量重合,且容易用错。
避免使用读写锁,实际上读写锁的性能并不见得有多高。
线程标识符
posix thread库提供了pthread_self
函数用于返回当前线程标识符,其类型为pthread_t
。
pthread_t
不一定是一个数值类型,也有可能是一个结构体。
- 无法打印输出
pthread_t
,没法在日志中使用 - 无法比较
pthread_t
的大小或者计算其hash,因此没法用作关联容器的key - 无法定义一个
pthread_t
值 -
pthread_t
值只在进程内有意义,与操作系统的任务调度之间无法建立有效关系 - 另外glibc的 pthread 实现实际上把
pthread_t
用作一个结构体指针,指向一块动态分配的内存
上面程序输出的是一样的值。int main() { pthread_t t1, t2; pthread_create(&t1, NULL, threadFunc, NULL); printf("%lx\n", t1); pthread_join(t1, NULL); pthread_create(&t2, NULL, threadFunc, NULL); printf("%lx\n", t2); pthread_join(t2, NULL); }
在Linux上,建议使用gettid()
系统调用的返回值作为线程id,这么做的好处有:
- 它的类型是pid_t,其值通常是一个整数,可以在日志中输出。
- 在Linux中,它直接表示内核的任务调度ID。
- 在其他系统工具中可以很容易定位到具体的某一个线程,例如在top中可以找出CPU使用率较高的线程ID。
- 任何时刻都是全局唯一的。
- 0是非法值,因为第一个进程init的pid 是1。
线程的创建与销毁守则
-
在进入
main
函数之前不应该启动线程
因为会影响到全局对象的安全构造,C++保证在进入main
之前完成全局对象的构造,但是,各个编译单元之间的对象构造顺序是不确定的。 -
程序中线程的创建最好能在初始化阶段全部完成
多线程与IO
在进行多线程网络编程的时候,一个自然的问题是:如何处理IO?能否多个线程同时读写同一个socket文件描述符?
首先,操作文件描述符的系统调用本身是线程安全的,但是多个线程同时操作同一个socket 文件描述符是很麻烦的,需要考虑:
- 如果一个线程正在阻塞地
read
某个socket,而另一个线程close
了这个socket。 - 如果一个线程正在阻塞地
accept
某个socket,而另一个线程close
了这个socket。 - 一个线程正准备
read
某个socket,而另一个线程close
了这个socket,第三个线程又恰好open
了另一个文件描述符,其fd号码正好与前面地socket相同,而这显然程序地逻辑都不对了。 - 如果两个线程同时read同一个TCP socket,两个线程几乎同时各自收到一部分数据,如何把数据拼成完整地消息?如何知道哪部分数据先到达。
所以为了简单起见,多线程程应该遵循的原则是:每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序性问题,也避免关闭文件描述符的各种race condition
。
善用__thread 关键字
__thread 是gcc内置的线程局部存储设施,它的使用规则:
- 只能用于修饰POD类型,不能修饰
class
类型,因为无法自动调用构造函数和析构函数 - __thread 可以用于修饰全局变量,函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量
- __thread 变量的初始化只能用于编译期常量
__thread 变量保证每个线程有一份独立的实体,各个线程的变量值互不干扰。
网友评论