Linux 进程与线程(二)线程
简介
线程
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中。RTOS中的任务就是线程的一种表现形式。
线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。
同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。
在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被 CPU 执行,线程具有以下一些特点:
- 不单独存在,包含在进程中
- 线程是参与系统调度的基本单位
- 同一进程的多线程可并发执行,宏观上实现同时运行的效果
- 同一进程的各个线程共享该进程拥有的资源:
- 所有线程具有相同的地址空间
- 线程可访问该地址空间的每一个虚地址
- 可访问进程所拥有的已打开文件、定时器和信号量
线程与进程的优劣
多进程编程的劣势:
- 内存空间不共享:每个进程有独立的虚拟地址空间,数据共享麻烦,必须通过 IPC(如管道、共享内存、消息队列等)
- 创建开销大:使用 fork() 创建新进程代价高(复制页表、上下文等)。频繁创建/销毁进程会显著降低性能
- 通信复杂:不同进程间通信(IPC)机制种类多、实现复杂,尤其是同步控制(例如多个进程同时写共享内存)容易出错
- 切换成本高:进程上下文切换比线程慢很多,因为需要切换整个地址空间、页表等
- 资源管理复杂:每个进程资源独立,协调管理(如关闭文件描述符、回收子进程)更复杂
多线程编程的优势:
- 共享地址空间:线程共享进程的内存,通信和共享数据非常方便,比如共享变量、缓冲区
- 创建/销毁成本小:创建线程用 pthread_create() 等,比 fork() 快很多,开销更小
- 上下文切换快:线程切换只需切换寄存器、栈等,不涉及页表等操作,切换代价低
- 通信简单:直接通过全局变量或堆内存通信,不需要复杂的 IPC 机制
- 适合高并发任务:在 Web 服务器、实时系统中常见,用来处理高并发连接更高效
多线程编程的劣势:
- 数据竞争:线程共享数据,必须使用互斥锁、读写锁等手段保护,稍不注意就出 bug
- 调试困难:多线程问题(死锁、竞态条件)很难重现和排查
- 一个线程崩溃可能导致整个进程崩溃
并发和并行
- 串行:一件事、一件事接着做
- 只用一口锅,先做完第一碗面,再做第二碗,最后做第三碗
- 并发:交替做不同的事
- 烧水的时候准备下碗;水烧着时你不闲着;一会煮面,一会加料;只要锅没闲着,切来切去地忙
- 并行:同时做不同的事
- 3个厨师 + 3口锅,每人煮一碗面,互不干扰
对于单核处理器,只有一个执行单元,因此同时只能执行一条指令;对于多核处理器,可并行执行多条指令,譬如8核处理器可并行执行8条不同的指令。
线程ID
1 |
|
该函数返回当前线程的线程ID。
还可以使用pthread_equal()
函数检查两个线程ID是否相等:
1 |
|
如果相等,返回非零值;否则返回0.
创建线程
1 |
|
形参:
thread
:函数成功返回时,创建成功的线程ID会保存在thread指向的内存中attr
:定义了线程的各种属性,如果设置为NULL,表示所有属性设为默认值start_routine
:新创建的线程从start_routine()函数开始执行arg
:要传递给start_routine()函数的参数,一般情况下需要将arg
指向一个全局或堆变量,即需要保证线程的生命周期中arg
指向的对象必须存在,否则会出现段错误。如果设置为NULL,表示不需要传入参数。
和进程创建时相同,一旦线程创建成功,新线程就会立即被加入到系统调度队列并开始执行,通常无法确定系统会首先调度哪一个线程来使用CPU资源(主线程或新创建的线程)。如果程序对不同线程的执行顺序有要求,就必须采用一些同步技术来实现。
简单的创建线程的例子:
1 |
|
终止线程
线程可以通过以下方式终止:
- 线程的start函数执行return并返回指定值,返回值就是线程的退出码
- 线程调用
pthread_exit()
函数 - 调用
pthread_cancel()
取消线程
pthread_exit()
将终止调用它的线程:
1 |
|
形参retval指定了线程的返回值,也就是线程的退出码。retval指向的内容不应分配在线程栈中,因为线程终止后将无法确认线程栈中的内存还是否有效。
回收线程
可调用pthread_join()
函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源:
1 |
|
retval
:如果不为NULL,函数将目标线程的退出状态复制到*retval
;如果目标线程被cancel,则将PTHREAD_CANCELED
放在*retval
中;如果对目标线程的终止状态不感兴趣,可设置为NULL
pthread_join()
类似于针对进程的waitpid()
,但是:
- 线程之间关系是平等的。同一进程中的任意线程均可调用
pthread_join()
来等待另一个线程的终止 - 不能以非阻塞的方式调用
pthread_join()
。pthread_join()
一旦被调用,对应线程即阻塞。
例子:
1 |
|
取消线程
取消线程特指某一个线程向另一个线程发送请求,要求其立即退出的操作。
取消一个线程
通过pthread_cancel()
:
1 |
|
该函数仅提出请求,不会等待目标线程退出,而是会立即返回。
取消状态和类型
1 |
|
pthread_setcancelstate
和pthread_setcanceltype
可用于设置线程的取消性状态和类型。pthread_setcancelstate
会将调用线程的取消性状态设置为state,并将线程之前的取消性状态保存在oldstate中。如果对旧状态不感兴趣,可设置为NULL。state必须是以下值之一:
PTHREAD_CANCEL_ENABLE
:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的PTHREAD_CANCEL_DISABLE
:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE
示例:
1 |
|
如果线程的取消性状态为ENABLE,那么其对取消请求的处理取决于线程的取消性类型,可通过pthread_setcanceltype()
来设置。type必须是以下值之一:
PTHREAD_CANCEL_DEFERRED
:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point)为止,这是所有新建线程包括主线程默认的取消性类型。PTHREAD_CANCEL_ASYNCHRONOUS
:很少使用。
取消点
当线程可以取消并且canceltype设置为PTHREAD_CANCEL_DEFERRED
时,当收到其他线程通过pthread_cancel()
发送到来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
取消点的作用是更精确的设置响应取消请求的位置,未到达取消点时,已经到达的取消请求将不会被响应。这是系统的一种保护机制,用于保证取消请求不会影响到关键代码的执行。man 7 pthreads
可查询可用作取消点的函数。
可取消性的检测
如果线程执行的是一个无取消点的死循环,比如while(1)
,那么似乎这时候线程永远都不会响应外部的取消请求;除了线程自己主动退出,其他线程将无法通过向它发送取消请求而终止它。
此时可以使用pthread_testcancel
:
1 |
|
该函数会产生一个取消点:
1 |
|
分离线程
默认情况下,当线程终止时,其它线程可以通过调用pthread_join()
获取其返回状态、回收线程资源。有时,代码并不关心线程的返回状态,也不打算join它,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用pthread_detach()
将指定线程进行分离,也就是分离线程:
1 |
|
一个线程可以将另一个线程分离,也可以将自己分离:pthread_detach(pthread_self())
分离之后的线程无法通过pthread_join
获取终止状态,该过程不可逆,分离之后就无法恢复到之前的状态。处于分离状态的线程终止后能够自动回收线程资源。
线程的分离状态通过pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
设置,参数可选为:
PTHREAD_CREATE_DETACHED
:分离线程PTHREAD _CREATE_JOINABLE
:非分离线程
注意:如果一个线程被设置为分离线程,而该线程运行又非常快,很有可能在pthread_create()
返回之前该线程就终止了,终止之后它就可能将线程号和系统资源移交给其他线程,这样pthread_create()
可能就会返回错误的线程号。避免这种情况最常见的措施就是同步通知,比如在被创建的线程里调用pthread_cond_timedwait()
函数,让线程等待一会儿,留出足够的时间让
pthread_create()
返回:
1 |
|
cond
:条件变量指针,是一个事件通知器,用于判断某种条件是否成立mutex
:互斥锁指针abstime
:绝对时间,等待超时的截止时间,单位为秒+纳秒
注意:pthread_cond_timedwait()
等待的是 “到指定时间点” 为止,而不是 “等待N秒”!
pthread_cond_timedwait()
本质上是一个带锁的同步等待机制,目的是等待某个线程间的“条件”成立。它会阻塞线程,但可以通过事件唤醒,还可以设置超时时间,防止死锁。适合:
- 等待某个事件的发生,如数据就绪或任务完成
- 在一段时间内等待,而非无限等待
- 希望在等待期间自动释放资源
该函数一般不能用sleep()
和wait()
代替:
sleep()
只是简单的挂起线程,无法被事件唤醒,也无法和条件变量配合使用wait()
用于进程,不能用于线程同步
应用示例:
1 |
|
线程清理函数(Thread cleanup handler)
线程可通过函数 pthread_cleanup_push()
和 pthread_cleanup_pop()
分别负责向调用线程的清理函数栈中添加和移除清理函数:
1 |
|
pthread_cleanup_push()
用于注册一个清理函数routine(arg)
,pthread_cleanup_pop()
负责移除栈顶的cleanup handler,如果excute != 0
,则调用它。
线程执行以下动作时,清理函数栈中的清理函数才会被执行:
- 调用
pthread_exit()
退出时 - 响应取消请求时(
pthread_cancel
) - 用非0参数调用
pthread_cleanup_pop()
pthread_cleanup_push()
和 pthread_cleanup_pop()
实际上是两个宏,必须成对出现,并在同一个函数作用域中使用。
示例:
1 |
|
线程属性
线程属性是pthread_attr_t
类型的,其在线程创建前被创建、初始化和修改,并在线程创建时通过形参传入线程。
初始化与销毁属性对象
-
int pthread_attr_init(pthread_attr_t *attr);
// 初始化线程属性对象 -
int pthread_attr_destroy(pthread_attr_t *attr);
// 销毁属性对象,释放资源
设置线程是否可分离(joinable/detached)
-
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 设置线程分离属性(PTHREAD_CREATE_JOINABLE
或PTHREAD_CREATE_DETACHED
) -
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
// 获取当前分离属性状态
设置线程栈大小与起始地址
-
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
// 设置线程栈大小 -
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
// 获取线程栈大小 -
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
// 设置线程栈地址和大小 -
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
// 获取线程栈地址和大小
设置线程调度策略和参数
-
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
// 设置调度策略(如SCHED_FIFO
,SCHED_RR
,SCHED_OTHER
) -
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
// 获取调度策略 -
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
// 设置调度参数(如优先级) -
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
// 获取调度参数
设置调度继承属性
-
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);
// 设置调度属性是否继承(PTHREAD_INHERIT_SCHED
或PTHREAD_EXPLICIT_SCHED
) -
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);
// 获取调度继承属性
设置线程作用域(系统级 / 进程级)
-
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
// 设置作用域(PTHREAD_SCOPE_SYSTEM
或PTHREAD_SCOPE_PROCESS
) -
int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);
// 获取线程作用域
代码范例
1 |
|