总结《Linux高性能服务器编程》14章
第14章 多线程编程
Linux线程概述
-
线程模型
进程:资源分配的最小单位
线程:程序执行的最小单位
-
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体;
-
根据运行环境和调度者的身份,线程可分为==内核线程和用户线程==
- 内核线程(也称LWP):运行在内核空间,由内核调度;
- 用户线程:运行在用户空间,由线程库来调度;
-
当进程的一个内核线程获得CPU的使用权时就加载并运行一个用户线程,内核线程相当于用户线程运行的“容器”;
-
一个进程可以拥有M个内核线程和N个用户线程,其中M≤N,按照M:N的取值,线程的实现方式可分为三种模式:
- M=1:完全在用户空间实现的线程
- 无须内核支持,由线程库负责管理;
- 一个进程的所有执行线程共享该进程的时间片,对外表现出相同的优先级,即该内核线程就是进程本身;
- 优点:创建和调度线程都无须内核干预,速度快,不占用额外内核资源;
- 缺点:一个进程的多个线程无法运行在不同的CPU上,线程的优先级只对同一个进程中的线程有效;
- M:N=1:1:完全由内核调度的模式
- 创建、由内核调度线程,运行在用户空间的线程库无须执行管理任务;
- 优缺点与M=1时的情况互换;
- 双层调度模式:前两种实现模式的混合体
- 内核调度M个内核线程,线程库调度N个用户线程;
- 结合优点:不会消耗过多内核资源,线程切换速度较快,可以充分利用多处理器优势;
- M=1:完全在用户空间实现的线程
-
-
Linux线程库
- Linux上两个最有名的线程库是LinuxThreads和NPTL,均采用1:1的方式实现;
- LinuxThreads线程库的内核线程是用clone系统调用创建的进程模拟的,但存在语义问题;
- 现代Linux上默认使用的线程库是NPTL;
- 内核线程不再是一个进程,避免使用进程模拟内核线程的语义问题;
- 摒弃管理线程,终止线程、回收线程堆栈等工作都可以由内核来完成;
- 一个进程的线程可以运行在不同的CPU上,从而充分利用多处理器系统的优势;
- 线程的同步由内核来完成,可实现跨进程的线程同步;
- Linux上两个最有名的线程库是LinuxThreads和NPTL,均采用1:1的方式实现;
创建线程和结束线程
-
创建和结束线程的基础API都定义在pthread.h头文件中;
-
创建线程的函数pthread_create
#include<pthread.h> int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine) (void*), void* arg);
- thread参数是新线程的标识符,pthread_t是一个整型类型;
- attr参数用于设置新线程的属性;
- start_routine和arg参数分别指定新线程将运行的函数及其参数;
- 一个用户可以打开的线程数量不能超过RLIMIT_NPROC软资源限制,系统上所有用户能创建的线程总数也不得超过/proc/sys/kernel/threads-max内核参数所定义的值;
-
结束线程的函数pthread_exit
#include<pthread.h> void pthread_exit(void* retval);
- 通过retval参数向线程的回收者传递其退出信息;
- 执行完之后不会返回到调用者,而且永远不会失败;
-
回收其他线程的函数pthread_join
#include<pthread.h> int pthread_join(pthread_t thread,void**retval);
- 类似于回收进程的wait和waitpid系统调用;
- 该函数会一直阻塞,直到被回收的线程结束为止;
-
取消线程的函数pthread_cancel
#include<pthread.h> int pthread_cancel(pthread_t thread);
-
接收到取消请求的目标线程可以决定是否允许被取消以及如何取消
#include<pthread.h> int pthread_setcancelstate(int state,int* oldstate); //state设置是否允许被取消 int pthread_setcanceltype(int type,int* oldtype);//type设置如何被取消(同步异步)
-
线程属性
-
pthread_attr_t结构体定义了一套完整的线程属性,全部包含在一个字符数组中
#include<bits/pthreadtypes.h> #define__SIZEOF_PTHREAD_ATTR_T 36 typedef union { char__size[__SIZEOF_PTHREAD_ATTR_T]; long int__align; }pthread_attr_t;
-
线程库定义了一系列函数来获取和设置线程属性
#include<pthread.h> /*初始化线程属性对象*/ int pthread_attr_init(pthread_attr_t*attr); /*销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用*/ int pthread_attr_destroy(pthread_attr_t*attr); /*下面这些函数用于获取和设置线程属性对象的某个属性*/ int pthread_attr_getdetachstate(const pthread_attr_t*attr,int*detachstate); int pthread_attr_setdetachstate(pthread_attr_t*attr,int detachstate); int pthread_attr_getstackaddr(const pthread_attr_t*attr,void**stackaddr); int pthread_attr_setstackaddr(pthread_attr_t*attr,void*stackaddr); int pthread_attr_getstacksize(const pthread_attr_t*attr,size_t*stacksize); int pthread_attr_getstack(const pthread_attr_t*attr,void**stackaddr,size_t*stacksize);
-
线程属性的含义
- detachstate:线程的脱离状态,可以指定为 [可被回收] 或 [脱离与进程中其他线程的同步] ;
- stackaddr和stacksize:线程堆栈的起始地址和大小;
- 可以使用ulimt-s命令来查看或修改堆栈空间大小(一般是8 MB);
- guardsize:保护区域大小,作为保护堆栈不会被错误覆盖的区域;
- schedparam:线程调度参数,表示线程允许优先级;
- schedpolicy:线程调度策略;
- inheritsched:是否继承调用线程的调度属性;
- scope:线程间竞争CPU的范围,即线程优先级的有效范围;
POSIX信号量
-
用于线程同步的机制:POSIX信号量、互斥量和条件变量;
-
在Linux上,信号量API有两组,一组是第13章讨论过的System V IPC信号量,另外一组POSIX信号量,接口相似,语义相同,但不能保证互换;
-
POSIX信号量函数的名字都以**sem_**开头;
-
常用的POSIX信号量函数(参数sem指向被操作的信号量):
-
初始化一个未命名的信号量
#include<semaphore.h> int sem_init(sem_t*sem,int pshared,unsigned int value);
- pshared参数指定信号量的类型,0表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享;
- value参数指定信号量的初始值;
-
销毁信号量,释放其占用的内核资源
#include<semaphore.h> int sem_destroy(sem_t*sem);
-
以原子操作的方式将信号量的值减1
#include<semaphore.h> int sem_wait(sem_t*sem); //信号量为0则阻塞 int sem_trywait(sem_t*sem); //始终立即返回,当信号量为0将返回-1并设置errno为EAGAIN
-
以原子操作的方式将信号量的值加1
#include<semaphore.h> int sem_post(sem_t*sem);
-
互斥锁(量)
-
互斥锁是用于同步线程对共享数据的访问;
-
互斥锁(也称互斥量)可以用于保护关键代码段,以确保其独占式访问;
-
当进入关键代码段时,需要获得互斥锁并将其加锁,这等价于二进制信号量的P操作;当离开关键代码段时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程,这等价于二进制信号量的V操作;
-
互斥锁基础API
#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_trylock(pthread_mutex_t*mutex); int pthread_mutex_unlock(pthread_mutex_t*mutex);
-
互斥锁属性
-
pshared:指定是否允许跨进程共享互斥锁
int pthread_mutexattr_getpshared(pthread_mutexattr_t*attr, int*pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t*attr, int pshared);
-
type:指定互斥锁的类型
int pthread_mutexattr_gettype(pthread_mutexattr_t*attr,int*type); int pthread_mutexattr_settype(pthread_mutexattr_t*attr,int type);
-
-
互斥锁类型
- 普通锁:默认,保证资源分配公平性,但如果对已经加锁的普通锁再次加锁,将引发死锁;
- 检错锁:如果对已经加锁的检错锁再次加锁则加锁操作返回EDEADLK,对已解锁的检错锁再次解锁,将返回EPERM;
- 嵌套锁:允许一个线程在释放锁之前多次对它加锁而不发生死锁,但获得这个锁需要执行相应次数的解锁操作;
-
死锁
- 对已经加锁的普通锁再次加锁,将引发死锁;
- 两个线程互相占有且等待互斥锁,将引发死锁;
条件变量
-
用于在线程之间同步共享数据的值;
-
当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程;
-
条件变量API
#include<pthread.h> int pthread_cond_init(pthread_cond_t*cond,const pthread_condattr_t*cond_attr); int pthread_cond_destroy(pthread_cond_t*cond); int pthread_cond_broadcast(pthread_cond_t*cond); int pthread_cond_signal(pthread_cond_t*cond); int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);
- 参数cond指向要操作的目标条件变量,条件变量的类型是pthread_cond_t结构体;
- pthread_cond_broadcast函数以广播的方式唤醒所有等待目标条件变量的线程;
- pthread_cond_wait函数用于等待目标条件变量;
多线程环境
-
可重入函数
- 可重入函数:一个函数能被多个线程同时调用且不发生竞态条件,则称它是线程安全的(thread safe);
- linux库函数只有一小部分是不可重入的,如getservbyname和getservbyport函数等;
-
线程和进程
- 一个多线程程序的某个线程调用了fork函数,新创建的子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制;
- 子进程将自动继承父进程中互斥锁(条件变量与之类似)的状态,但可能不清楚从父进程继承而来的互斥锁的具体状态;
- pthread提供了一个pthread_atfork函数,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态;
-
线程和信号
-
每个线程都可以独立地设置信号掩码
#include<pthread.h> #include<signal.h> int pthread_sigmask(int how,const sigset_t*newmask,sigset_t*oldmask);
-
线程库将根据线程掩码决定把信号发送给哪个具体的线程;
-
进程中的所有线程共享该进程的信号和信号处理函数,应该定义一个专门的线程来处理所有的信号。
-