1.多线程概述
基础概念
线程
- 进程里执行代码的部分;
- 包含一系列机器指令所必须的机器状态,包括当前指令位置(一般为PC寄存器)、栈顶指针SP、通用寄存器、地址和数据寄存器等
- 线程不包括进程中的其他数据,如地址空间和文件描述符
进程
- 线程加上地址空间、文件描述符和其他数据
- 一个进程中的所有线程共享文件和地址空间,包括程序段、数据段和堆栈
进程 vs 线程
- 多个线程可以共享一个地址空间,而做不同的事情
- 在多处理器系统中,一个进程中的多个线程可以同时做不同的工作
- 系统在线程间切换比在进程间切换快得多
- 每一个进程有独立的虚拟地址空间,但同一个进程中的线程共享相同的地址空间和其他进程数据
异步(asynchronous)
事情相互独立地发生,除非有强加的依赖性。任何两个彼此独立运行的操作都是异步的。
- 如果没有同时执行多个活动,那么异步就没有优势
- 如果开始了一个异步活动,然后什么也不做就等待他结束,则并没有从异步获得好处
并发(concurrency)
- 实际上可能是串行发生的事情好像同时发生一样
- 并发描述的是单处理器系统中线程或进程的行为
- 在POSIX中,并发的定义要求“延迟调用线程的函数不应该导致其他线程的无限期延迟”
- 并发操作之间可能任意交错,导致程序相互独立的运行(一个程序不必等到另一个程序结束后才开始运行),但并发并不代表操作同时进行
并发 vs 并行
并发:指并发序列同时执行。指事情在相同的方向上独立进行(没有交错)。
- 真正的并行只能在多处理器系统中存在
- 但并发可以在单处理器系统和多处理器系统中都存在
- 并发能在单处理器系统中存在是因为并发实际上是并行的假象
- 并行则要求程序能够同时执行多个操作
- 而并发只要求程序能够假装同时执行多个操作
线程安全
什么是线程安全?
- 定义:指代码能够被多个线程调用而不会产生灾难性后果
- 特点:不要求代码在多个线程中高效的运行,只要求能够安全地运行
实现线程安全的工具
pthreads 互斥量、条件变量、线程私有数据
如何实现线程安全?
- 一般方法:
- 对不需要保存永久状态的函数,通过整个函数调用的串行化实现
- 比如,进入函数时加锁,退出函数时解锁
- => 函数可以被多个线程调用,但一次只能有一个线程调用该函数
- 更有效的方式
- 将线程安全函数分为多个小的临界区
- => 允许多个线程进入该函数,但不能同时进入一个临界区
- 更好的方式
- 将代码改造为对临界对象(数据)的保护而非对临界代码的保护
- => 可使不同时访问相同临界数据的线程完全并行的执行
可重入
可重入
- 有时用来表示有效的线程安全,即通过采用比将函数或库转换成一系列区域更为复杂的方式使代码成为线程安全的
- 可重入的函数应该避免依赖任何静态数据,最好避免依赖线程间任何形式的同步
- 互斥量和线程私有数据可以实现线程安全,但通常需要改变接口来使函数可重入
举例
- pthreads 为了使 readdir() 函数可重入,增加 readdir_r() 函数,并在该函数内避免任何锁操作
- 让调用者在搜索目录时分配一个数据结构来保存 readdir_r() 的环境
特点
这种方式只有调用者才知道数据如何使用。
并发系统基本功能
基本功能
- 执行环境:是并发实体的状态;提供建立、删除、维护环境的方式
- 调度:决定在某个给定时刻该执行哪个环境,并在不同的环境中切换
- 同步:为并发执行的环境提供协调访问共享资源的机制
什么是同步?——让线程协调地完成工作的机制
同步的实现方式
- 互斥量
- 条件变量
- 信号量
- 事件
- 消息机制:管道/Socket/Posix消息队列
线程、互斥量、条件变量关系
- 线程是计算机中的可执行单元,是 CPU 调度单位
- 互斥量和条件变量都是线程同步的手段
- 互斥量阻止线程间发生不可预期的冲突
- 一旦避免了冲突,条件变量让线程等待直到可以安全地执行
2.线程建立与使用
创建线程
- 通过
pthread_create()
函数创建线程- 向该函数传递线程函数地址和线程函数参数
- 线程函数只有一个 void* 参数
- 该函数返回 pthread_t 类型的线程ID
- 一般调用该函数创建线程,然后调用
pthread_join()
函数等待线程结束- 在当前线程从函数
pthread_create()
中返回以及新线程被调度执行之间不存在同步关系 - 新线程可能在当前线程从
pthread_create()
返回值前就运行了 - 或在当前线程从
pthread_create()
返回之前,新线程就可能已经运行完毕了
- 在当前线程从函数
pthread_join()
- 阻塞其调用者直到指定线程终止,然后可以选择地保存线程的返回值
- 当
pthread_join()
调用返回时,被连接线程就已经被分离(detached),再也不能连接该线程了 - 如果连接(joining)线程不关心返回值,或者它知道被连接(joined)的线程根本不返回任何值,则可向
pthread_join()
的 &retval 参数传递 NULL,此时,被连接线程的返回值将被忽略
初始线程
- C 程序运行时,首先运行 main() 函数,main() 函数所在线程称为初始线程或主线程
- 初始线程可调用
pthread_self()
获得其 ID,也可调用pthread_exit()
来终止自己 - 从 main() 返回将导致进程终止,也将使进程内所用线程终止
- 在 main() 中调用
pthread_exit()
,这样进程就必须等待所有线程结束后才能终止 - 若初始线程将其 ID 保存在一个其他线程可以访问的空间,则其他线程就可以等待初始线程的终止或者分离初始线程
线程分离
- 分离一个正在运行的线程不会对线程带来任何影响,仅仅是通知系统当该线程结束时,其所属资源可以被回收
- 分离线程意味着通知系统不再需要此线程,允许系统将分配给它的资源回收
- 一个没有被分离的线程终止时会保留其虚拟内存,包括堆栈和其他系统资源
线程生命周期
在任意时刻,线程处于下表的四个基本状态之一。
状态 | 说明 |
就绪 ready | 线程能够运行,但在等待可用的处理器
|
运行 running | 线程正在运行;在多处理器系统中,可能有多个线程处于运行状态 |
阻塞 blocked | 线程由于等待处理器外的其他条件无法运行,如条件变量的改变、加锁互斥量或IO操作结束 |
终止 terminated | 不是被分离,也不是被连接,一旦线程被分离或者连接,它就可以被回收
|
线程状态转换如下图。
说明
- 线程开始处于就绪状态
- 当线程运行时,它调用特定的起始函数
- 它可能被其他线程抢占,或者因等待外来事情而阻塞自己
- 最终线程完成工作,或者从起始函数返回,或者调用
pthread_exit
函数,即进入终止状态 - 如果线程已被分离,则它立刻被回收重用;否则,线程停留在终止状态直到被分离或被连接
就绪态
- 线程刚被创建时
- 线程被解除阻塞再次可以运行时
- 运行线程被抢占时,如时间片到
被阻塞
- 试图加锁一个已经被锁住的互斥量
- 等待某个条件变量
- 调用 singwait 等待信号
- 执行无法立即完成的 IO 操作
- 内存页错误之类的系统操作
初始线程(main()函数所在线程)与普通线程区别
- 初始线程的启动函数 main() 是从程序外部调用的;如 crt0.o 文件复制初始化进程并调用 main() 函数;而普通线程的启动函数及其运行参数均由
pthread_create()
函数创建线程时传入,且由 CPU 调度的 - main()函数的参数是 argc 和 argv;普通线程的参数是 void*,且由
pthread_create()
函数传入 - 若普通线程从启动函数中返回,则线程终止,而其他线程依然可以运行;但初始线程从 main() 返回时,进程终止,进程内所有线程也被终止
- 若希望在初始线程终止时,进程中的其他线程继续执行,则需要在初始线程调中调用
pthread_exit()
而非从main()
返回 - 大多数系统,初始线程运行在默认进程堆栈上,该堆栈可以增长到足够尺寸;而某些实现中,普通线程的堆栈空间是受限的
- 如果线程堆栈溢出,则程序会出现段错误
线程睡眠原因
- 被阻塞,需要的某个资源不可用
- 被抢占,即系统将处理器分配给其他线程
pthread_join()
的详细解释
- 用来等待一个线程的结束;
- 是一个线程阻塞函数,调用它的函数将一直等待到被等待的线程结束为止
- 如,主线程调用
pthread_join()
等待它创建的线程运行结束,即主线程调用该函数后会被阻塞 - 当函数返回时,被等待的线程的资源被回收
- 若此时新线程没有运行,则它将在主线程被阻塞后从就绪态进入运行态;当新线程运行完毕并返回时,主线程才会被解除阻塞,返回就绪态;当处理器可用时,主线程或立即执行或等到创建的线程终止后重新运行直到结束
线程终止
- 一般地,线程从启动函数返回来终止自己
- 当调用
pthread_exit()
退出线程或者调用pthread_cancel()
取消线程时,线程在调用每个清理过程后也进入终止状态 - 清理过程又线程通过
pthread_cleanup_push()
注册,且尚未通过pthread_cleanup_poo()
删除
Linux 系统僵尸线程
- 如果线程已经被分离,则会被回收;否则,线程处于终止状态,仍然可以被其他线程调用
pthread_join()
连接 - 这种线程被称为僵尸线程,像 Uni 系统中的进程已经结束但还没有被一个
wait/waitpid
调用回收一样,即使已经死了但还存在 - 僵尸线程可能会保留其运行时的大部分甚至所有资源,因此不应该让线程长时间处于这种状态;当创建不需要连接的线程时,应该使用 detachstate 属性建立线程使其自动分离
线程回收
- 如果使用 detachstate 属性(即设置属性为 PTHREAD_CREATE_DETACH )建立线程,或者调用
pthread_detach()
分离线程,则当线程结束时将被立刻回收 - 如果终止线程没有被分离,则它将一直处于终止状态直到被分离(通过
pthread_detach
)或者被连接(通过pthread_join
) - 线程一旦被分离,就不能再访问它
- 回收将释放所有在线程终止时未释放的系统和进程资源,包括
- 保存线程返回值的内存空间、堆栈
- 保存寄存器状态的内存空间
- 实际上线程终止时上述资源就不能被访问了
- 一旦线程被回收,线程ID就无效了,不能再连接、取消或者执行其他任何操作
- 终止线程ID可能被分给新线程
3.线程常用API
简介
POSIX thread 简称为 pthread,Posix 线程是一个 POSIX 标准线程。该标准定义内部 API 创建和操纵线程。
作用
线程库实行了 POSIX 线程标准通常称为 pthreads. pthreads 是最常用的 POSIX 系统如 Linux 和 Unix,而微软 Windowsimplementations 同时存在。举例来说,pthreads-w32 可支持 MIDP 的 pthread。
Pthreads 定义了一套 C 程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现。
数据结构与函数
数据结构
pthread_t | 线程句柄 |
pthread_attr_t | 线程属性 |
线程操纵函数(省略参数)
pthread_create | 创建一个线程 |
pthread_exit | 终止当前线程 |
pthread_cancel | 中断另外一个线程的运行 |
pthread_join | 阻塞当前的线程,直到另外一个线程运行结束 |
pthread_attr_init | 初始化线程的属性 |
pthread_attr_setdetachstate | 设置脱离状态的属性(决定这个线程在终止时是否可以被结合) |
pthread_attr_getdetachstate | 获取脱离状态的属性 |
pthread_attr_destroy | 删除线程的属性 |
pthread_kill | 向线程发送一个终止信号 |
同步函数
用于 mutex 和条件变量
pthread_mutex_init | 初始化互斥锁 |
pthread_mutex_destroy | 删除互斥锁 |
pthread_mutex_lock | 占有互斥锁(阻塞操作) |
pthread_mutex_trylock | 试图占有互斥锁(不阻塞操作)。当互斥锁空闲时将占有该锁;否则立即返回 |
pthread_mutex_unlock | 释放互斥锁 |
pthread_cond_init | 初始化条件变量 |
pthread_cond_destroy | 销毁条件变量 |
pthread_cond_wait | 等待条件变量的特殊条件发生 |
pthread_cond_signal | 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程 |
Thread-local storage(线程特有数据)
pthread_key_create | 分配用于标识进程中线程特定数据的键 |
pthread_setspecific | 为指定线程特定数据键设置线程特定绑定 |
pthread_getspecific | 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中 |
pthread_key_delete | 销毁现有线程特定数据键 |
工具函数
pthread_equal | 对两个线程的线程标识号进行比较 |
pthread_detac | 分离线程 |
pthread_self | 查询线程自身线程标识号 |