该系列文章为《Linux/Unix系统编程手册》的学习笔记,由于该书太过冗长,属于工具书的类别,这里对书中的一些核心内容加以提炼和整理。 书中的编程练习这里不做展示和说明。
概念
线程:允许应用程序并发执行多个任务的一种机制,一个进程可以执行多个线程,这些线程执行相同的程序,共享一份全局内存,包括初始化数据段、未初始化数据段以及堆内存段。进程启动时有且只有单个线程,称为初始(initial)线程或者主(main)线程。
线程ID:线程的唯一标识(C语言中定义为pthread_t
),在Linux中,线程ID在所有进程中都是唯一的。
多个线程共享的主要属性有:
- 进程ID
- 控制终端
- 打开的文件描述符
- 信号处置
线程独占的属性有:
- 线程id
- CPU亲和力
- 栈
线程行为
创建
int pthread_creat(pthread_t *thread, const pthread_addr_t *addr, void *(*start)(void *), void *arg);
线程创建后会从start(arg)
开始执行,并继续执行调用之后的语句直到return
。与进程类似,创建进程后无法确定那个线程先获得CPU资源。
终止
有四种方式可以终止线程:
- 线程执行
start
函数的return
正常返回退出。 - 线程调用
pthread_exit()
。 - 调用
pthread_cancel()
取消线程。 - 任意线程调用
exit()
或者主线程执行了return
语句,会导致进程中所有的线程终止。
void pthread_exit(void *retval);
调用pthread_exit()
相当于执行start
函数中的return
提前返回退出。如果主线程执行pthread_exit()
,其他线程将继续运行。
连接
int pthread_join(pthread_t threadm void **retval);
调用pthread_join
来等待某个线程终止,如果该线程已经终止,join函数将立即返回,类似于进程的waitpid
,不过有两点区别:
- 线程之间关系是对等的。
- 无法“连接任意线程”,也无法“以非阻塞方式进行连接”。
僵尸线程:如果线程未分离,则必须使用pthread_join
进行连接。如果未进行连接的线程提前终止,这个线程将转变为僵尸线程,与僵尸进程概念类似。如果僵尸线程积累过多,除了浪费资源外,应用将无法创建新的线程。
分离
int pthread_detach(pthread_t thread);
如果不关心线程的返回状态,只是希望系统在线程结束时能够自动清理并移除,可以调用pthread_detach
进行分离。分离后的线程不能再被连接。当主线程调用exit
或return
后,即使分离过的线程也将立即终止。
取消
int pthread_cancel(pthread_t thread);
调用pthread_cancel
会使一个线程立即退出,函数将立即返回而不会等待目标线程退出。
进程与线程
优劣势
线程相比进程的优势在于:
- 线程数据共享简单。
- 线程创建的资源消耗小,创建速度比进程快10倍以上。
劣势:
- 需要保证线程安全。
- 线程会争用宿主进程的虚拟内存空间(每个线程的线程栈都会消耗进程的虚拟内存)。
在线程中执行进程方法
- 线程执行
exec
:任一线程调用exec
后调用进程将被完全替换,除了当前线程之外的所有线程都将立即消失。 - 线程执行
fork
:线程执行fork
后,仅会将当前线程复制到子进程中,其他线程均会在子进程中消失。
线程安全
- 临界区:访问某一共享变量的代码片段,这段代码片段执行的应为原子操作,同时访问同一共享资源的其他线程不应中断代码的执行。
- 互斥量:有两个状态,锁定和未锁定,可以被获取(acquire)和释放(release),可以确保共享变量同一时间只有一个线程访问。
- 条件变量:就共享变量的状态改变通知其他线程,并让其他线程等待这一通知。
- 线程安全:若函数可以同时供多个线程调用,则称为线程安全函数。
实现线程安全的方式主要有:采用互斥量或使用线程特有数据替代共享变量。可重入函数无需借助互斥量就可实现线程安全。
线程实现模型
线程实现有三种模型,三种模型的不通主要集中在线程如何与内核调度实体(KSE)映射,KSE是内核分配CPU及其他系统资源的基本单位。
多对一:用户级线程
线程创建、调度及同步的所有细节均有进程在用户空间的线程库处理,对于进程中的多个线程,内核一无所知。
优点:
- 由于不涉及中断,因此线程的创建、终止以及上下文切换、同步等速度都很快。
缺点:
- 系统调用会阻塞所有的线程。
- 无法将线程调度给多处理器,无法借助多核优势。
一对一:内核级线程
每个线程映射一个单独的KSE,内核分别对每个线程做调度处理,线程同步操作也通过内核实现。Linux的线程都采用此模型。
当应用程序中包含大量线程时,内核会为每个线程维护一个KSE,给内核调度器造成严重负担,大大降低系统的整体性能。
多对多:两级模型
结合了上述两种模型的有点,每个进程可以有多个KSE,也可以把多个线程映射到同一个KSE。但模型太过复杂,线程调度需要由内核及用户空间共同维护。
(End)