Linux线程概述
内核线程和用户线程
线程是程序中完成独立任务的完整执行序列。即一个可调度的实体,根据运行环境和调度者身份,线程分为内核线程和用户线程:
- 内核线程:在有的系统上也称为LWP(轻量级进程),运行在内核空间,由内核调度;
- 用户线程:运行在用户空间,由线程库调度;
当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程,可见,内核线程相当于用户线程运行的“容器”。
一个进程可以拥有M个内核线程和N个用户线程,其中M≤N,并且在一个系统的所有进程中,M和N的比值都是固定的,按照M:N的取值,线程的实现方式可分为三种模式:
- 完全在用户空间实现;
- 完全由内核调度;
- 双层调度;
完全在用户空间实现的线程无须内核的支持,内核甚至不知道这些线程的存在。线程库负责管理所有执行线程,比如线程的优先级、时间片等。线程库利用longjmp 来切换现成的执行,使它们看起来像是“并发”执行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说,一个进程的所有执行线程共享该进程的时间片,它们对外表现出相同的优先级,因此,对于这种实现方式而言,M个用户线程对应1个内核线程,而该内核线程就是进程本身。完全在用户空间实现的线程:
- 优点是:创建和调度线程无须内核的干预,因此速度相当快,并且它不占用额外的内核资源,所以即使创建了很多线程,也不会对系统性能造成明显的影响。
- 缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度单位来分配CPU的。此外线程的优先级只对同一个进程的线程有效,比较不同进程中的线程优先级是无意义的。
完全由内核调度的模式将创建、调度线程的任务都交给了内核,运行在用户空间的线程库无须执行管理任务。较早的Linux内核对内核线程的控制能力有限,线程库通常还要提供额外的控制,尤其是线程同步机制,不过现代Linux内核大大增强了对线程的控制,完全由内核调度的这种实现方式满足M:N = 1:1,即1个用户空间线程对应1个内核线程;
双层调度模式是前两种实现模式的混合体:内核调度M个内核线程,线程库调度N个用户线程,这样不但不会消耗过多的内核资源,而且线程切换速度也较快,同时它可以充分利用多处理器的优势;
线程操作函数
#include <pthread.h>
int pthread_create(pthread_t* thread, //unsigned long 整形类型
const pthread_attr_t* attr, //线程属性
void* (*start_routine)(void *),//线程运行函数
void* arg);//函数的参数
void pthread_exit(void* retval); //retval 传递退出信息
int pthread_join(pthread_t thread, void** retval); //阻塞等待线程结束
int pthread_cancel(pthread_t thread); //取消线程,接收到取消请求的目标线程可以决定是否被取消以及如何取消
int pthread_detach(pthread_t thread); //分离释放线程
线程相关函数详解
同步函数
/* 信号量 */
#include <semaphore.h>
int sem_init(sem_t* sem,
int pshared, //为0表示是当前进程的局部信号量
unsigned int value); //初始化信号量
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_post(sem_t* sem);
/* 互斥锁 */
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
//这样也可以初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 条件变量 */
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
线程安全的定义
一个线程安全的类应满足以下三个条件:
- 多个线程同时访问时,其表现出正确的行为;
- 无论OS如何调度这些线程,无论这些线程的执行顺序如何交织;
- 调用端代码无须额外的同步或其他协调动作;
根据这个定义,C++标准库里面的大多数class都不是线程安全的,包括string、vector、map等,因为这些类需要外部加锁才能供多个线程同时访问。
间不要泄露this指针。**
线程同步原则
- 尽量最低限度的共享对象,减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露,如果要暴露,优先使用immutable对象;实在不行才暴露可修改的对象,并用同步措施来充分保护它;
- 其实是使用高级的并发编程构件,如TaskQueue,Producter-Consumer Queue等等;
- 最后不得已必须使用底层同步原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量;
Singleton实现
template<typename T>
class Singleton{
public:
static T& instance(){
pthread_once(&ponce_, &Singleton::init);
return *value_;
}
private:
Singleton();
~Singleton();
static void init(){
value_ = new T();
}
private:
static pthread_once_t ponce_;
static T* value_;
};
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = NULL;
sleep()不是同步原语
生产代码中线程的等待可分为两种:
- 等待资源可用(要么等在select/poll/epoll_wait上,要么等在条件变量上);
- 等待进入临界区(等在mutex)以便读写共享数据;
后一种等待通常非常短,否则程序的性能和伸缩性就会有问题;
在程序的正常执行中,如果需要等待一段已知的时间,应该往event loop里注册一个timer,然后在timer的回调函数里接着干活,因为线程是个珍贵的共享资源,不能轻易浪费(阻塞也是浪费)。如果等待某个事件发生,那么应该采用条件变量或I/O事件回调,不能用sleep来轮询,不要采用这样的业余做法:
while(true){
if(!dataAvailable)
sleep(some_time);
else
consumeData();
}
如果多线程的安全性和效率要靠代码主动调用sleep来保证,这显然是设计出了问题。等待某个事件发生,正确的做法是用select()等价物或Condition,亦或高层同步工具,在用户态做轮询是低效的。
适用多线程程序的场景
多线程的应用场景是:提高响应速度,让IO和“计算”相互重叠,降低latency。虽然多线程不能提高绝对性能,但能提高平均响应性能;
一个程序要做成多线程的,大致要满足:
- 有多个CPU可用;
- 线程间有共享数据,即内存中的全局状态;
- 共享数据是可以修改的,而不是静态的常量表;
- 提供非均质的服务,即,事件的响应有优先级差异,可以用专门的线程来处理优先级高的事件,防止优先级反转;
- latency和throughput同样重要,不是逻辑简单的IO bound或CPU bound程序,换言之,程序要有相当的计算量;
- 利用异步操作,比如logging,无论往磁盘写log file,还是往log server发送消息都不应该阻塞critical path;
- 能scale up,一个好的多线程程序应该能享受增加CPU数目带来的好处;
- 具有可预测的性能,随着负载增加,性能缓慢下降,超过某个临界点之后急速下降,线程数目一般不随负载变化;
- 多线程能优先的划分责任和功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是把所有逻辑都塞到一个event loop里,不同类别的时间之间相互影响。
示例:
假设要管理一个Linux服务器机群,机群里面有8个计算节点,1个控制节点,机器的配置都是一样的,双路四核CPU,千兆网互联。现在需要编写一个简单的机群管理软件,这个软件由三部分组成:
- 运行在控制节点的master,这个程序监视并控制整个机群的状态;
- 运行在每个计算节点的salve,负责启动和终止job,并监控本机的资源;
- 供最终用户使用的client命令行,用于提交job;
可知,slave是个“看门狗进程”,它会启动别的job进程,因此必须是个单线程进程,另外它不应该占据太多CPU资源,这也适合单线程模型,master应该是个多线程程序:
线程的分类
一个多线程服务程序的线程大致可分为3类:
- IO线程:这类线程的主循环是IO multiplexing,阻塞的等在select/poll/epoll_wait系统调用上,这类线程也处理定时事件,当然它的功能不止IO,有些简单的计算也可以放入其中;
- 计算线程:这类线程的主循环是blocking queue,阻塞的等在condition variable上。这类线程一般位于thread pool中,通常不涉及IO,一般要避免任何阻塞操作;
- 第三方库所用的线程,比如logging、database connection;
线程池大小的阻抗匹配原则
如果池中线程在执行任务时,密集计算所占的时间比重不过为P(0 < p ≤ 1),而系统一共有C个CPU,为了让C个CPU跑满又不过载,线程池大小的经验公式为T = C / P;考虑到P的值估计不是很准确,T的最佳值可以上下浮动50%。
当P < 0.2,这个公式就不适用了,T可以取一个固定值,比如5*C,C不一定是CPU总数,可以是“分配给这项任务的CPU数”;
多线程系统编程精要
学习多线程编程面临的最大的思维方式的转变有两点:
- 当前线程可能随时会被切换出去,或者说被强占了;
- 多线程程序中事件发生顺序不再有全局统一的前后关系;
在没有适当同步的情况下,多个CPU上运行的多个线程中的事件发生先后顺序是无法确定的;
多线程的正确性不能依赖于任何一个线程的执行速度,不能通过原地等待(sleep())来假定其他线程的事件已经发生,而必须通过适当的同步来让当前线程能看到其他线程的事件的结果。无论线程执行的快与慢(被OS切换出去得越多,执行越慢),程序都应该正常运行;
不必担心系统调用的线程安全性,因为系统调用对于用户态程序来说是原子的。但是要注意系统调用对于内核状态的改变可能影响其他线程。
编写一个线程安全程序的难点在于线程安全是不可组合的,尽管单个函数是线程安全的,但两个或多个函数放到一起就不再安全了。
可参考内容