线程是最小执行单位, 进程是最小分配资源单位. 线程由进程退化而来(进程调用pthread_create), 多个线程同享同一份虚拟地址空间, 每一个线程有一个独立的PCB. 对CPU而言, 进程和线程的地位是等同的(都有PCB), 所以一个进程退化成多个线程后能够分到更多的时间片, 从而提高效率. 各线程共享文件描述符表和除栈以外的用户地址空间; 不共享内核栈/用户栈,信号屏蔽字,errno变量.
使用多线程模型时应避免使用fork()和信号, 否则容易出错.
三级页表
PCB内页目录的指针指向中间页目录(4k), 中间页目录的每一个指针指向一个页表, 页表的每一个指针又指向一个物理页面, 物理页面内部就是存储数据的内存单元. 页目录 -> 中间页目录 -> 页表 -> 物理页面, 该结构称为三级页表. 物理页面和虚拟地址空间通过MMU映射起来, MMU记录页码和页框码以及页内偏移量, 经常访问的页面会存放到快表寄存器中.
栈帧和寄存器
线程虽然独享PCB, 但是它的页目录页表还是跟原进程相同的, 所以多个线程共享虚拟地址空间, 指向相同的内存. 线程负责执行函数, 所以可以看做寄存器和栈的集合, 每一个函数在栈空间内都有一个栈帧(通过寄存器指针esp - ebp得到), 由于对应的寄存器只有一个, 所以栈帧内会存储之前ebp/esp的值. 线程也要在内核栈内保存寄存器的值, 用来切换线程.
线程控制原语
pthread库是第三方库, 编译链接时要加上-lpthread参数.
pthread_self()
查看进程ID, 相当于getpid(). 注意线程号LWP不是线程ID, 线程号是CPU分配时间片的依据, 线程ID用来在进程中区分线程. 使用ps -Lf pid
查看线程号, 其中pid是进程号, 可以看到该进程下各线程的LWP.
pthread_create(&tid, NULL, 线程主函数, 线程主函数参数)
创建线程, 相当于fork(). 成功返回0, 失败返回错误号, 不是-1, 可以使用strerror(errno)
打印错误信息. 线程主函数为void* (*func)(void*)
类型的函数指针. 参数必须传值进去如(void*)i, 传地址可能是在争抢中其他线程的参数.
调用pthread_create()的进程退化为主控线程, 与其他线程的pid相同, 线程ID各不同.
pthread_exit((void*)retval)
单个线程自己退出, 等价于exit(), 参数retval为线程退出时传出的值. 线程中最好不要使用return, 绝不能使用exit(), exit()会将整个进程退出.
pthread_join(tid, (void**)&retval)
等价于waitpid(), 阻塞等待线程关闭, 回收线程, 可以是兄弟线程调用. 传出参数&retval同上, 类似于&status, 也是线程退出时传出的值. 根据如何退出的retval会存放不同的值, 如return存放返回值, pthread
_cancel()存放PTHREAD_CANCELED, pthread_exit()存放exit传出的retval.
pthread_detach(tid)
使线程与主控线程断开关系, 从而不需要(也不能被)其他进程pthread_join()就可以自己结束并回收所有资源(清理PCB), 不会导致僵尸进程.
pthread_cancel(tid)
等价于kill(), 杀死线程. 线程不会立刻被杀死, 而是在系统调用处如open()/read()后才被杀死. 可以自己添加取消点pthread_testcancel()
.
修改线程属性
- 修改线程为分离态: 除了detach函数也可以在create的时候就指定为分离态
pthread_attr_t是一个结构体, 需要有初始化init和对应的destroy函数, 在setdetachstate后传入到create中.
- 修改线程栈的大小
线程同步
互斥量
pthread_mutex_t mutex
定义锁, 是结构体类型, 可看作只有0/1的整数. 初始化成功为1. 应定义成全局变量.
pthread_mutex_init(&mutex, NULL)
对互斥锁进行初始化为1, 要在create线程之前, 声明和初始化如果定义成全局/静态变量可以用静态初始化替代pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
, 局部变量必须用动态初始化.
pthread_mutex_destroy(&mutex)
. 销毁互斥锁
pthread_mutex_lock(&mutex)
加锁, 将mutex-1变为0. 加锁失败会阻塞.
pthread_mutex_unlock(&mutex)
解锁, 将mutex+1变为1. 同时唤醒所有阻塞在该锁上的线程.
pthread_mutex_trylock(&mutex)
尝试非阻塞加锁, 如果锁已被占用则直接返回, 不会像lock一样阻塞.
在访问共享资源前加锁, 访问结束后立即解锁, 锁的粒度应越小越好.
死锁
-
连续对同一个互斥量加锁两次. 第二次请求加锁时会造成线程阻塞等待, 导致第一次加的锁无法释放.
解决方法: 加锁完立刻解锁, 第二次加锁前先检查是否已经有锁.
-
线程1拥有着A锁, 请求获得B锁; 线程2拥有着B锁, 请求获得A锁. 对于已经拥有互斥锁的线程, 再去请求别的锁, 会导致线程阻塞等待. 双方都不释放就会导致死锁.
预防方法:
- 一次申请两把锁, 之后一起释放.
- 当不能拿到所有的锁时, 主动释放已有的锁, 等别的线程把两个锁都释放后就可以继续加锁了.
- 对线程编号.
注意, trylock()不能解决问题, 因为即使不阻塞等待, 但两个锁仍未被两个线程释放, 所以仍然无法得到对方的锁.
死锁解决方法:
除了预防, 还可以1.抢占资源分配给死锁线程. 2.杀死线程, 打破循环.
哲学家吃饭问题:
策略1: 规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。按此规定,将是0、1号哲学家竞争1号筷子,2、3号哲学家竞争3号筷子。即五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获得两支筷子而进餐。
策略2: 至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。定义信号量count,只允许4个哲学家同时进餐,这样就能保证至少有一个哲学家可以就餐。一个主动放弃自己(可能)拥有的锁, 使别的线程获得. 使用信号量实现.
读写锁
读写锁同时具备两种状态: 读锁和写锁. 其特性为: 写独占, 读共享; 写锁优先级高.
举例: 当已经有线程对共享资源加了读锁时, 若其他线程也都是读者, 以读模式对其加锁的线程可以共享该线程的读锁, 一起读; 若其他线程都是写者, 则写者都阻塞; 若其他线程有读锁有写锁, 则写锁优先.
若共享资源已被加了写锁, 在写的时候别的线程都无法访问共享资源, 等到写锁释放后, 其他线程再争夺锁, 这时仍是写锁优先.
一个读写锁同时只能有一个写者或多个读者(与CPU数相关), 当有多个写者竞争时, 只有一个能获得写锁, 其他的写者下一次获得锁.
pthread_rwlock_init()
初始化
pthread_rwlock_destroy()
销毁, 主控线程内最后执行
pthread_rwlock_rdlock()
读锁
pthread_rwlock_tryrdlock()
尝试读
pthread_rwlock_wrlock()
写锁
pthread_rwlock_trywrlock()
尝试写
pthread_rwlock_unlock()
解锁, 不区分读写
rdlock若申请不到锁,则自旋(隔一会再看看),tryrdlock若申请不到锁,则返回(不会再尝试),由用户自旋.
条件变量
条件变量不是锁, 但会造成线程阻塞. 通常与互斥锁配合使用, 实现生产者消费者模型. 条件变量的优点是减少生产者和消费者,消费者和消费者之间对互斥量无意义的竞争, 使线程挂起释放CPU, 提高效率.
pthread_cond_t cond
条件变量, 结构体类型
pthread_cond_init(cond, NULL)
初始化, 声明和初始化如果定义成全局/静态变量也可以用静态初始化替代pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_destroy(cond)
摧毁
pthread_cond_wait(cond, mutex)
阻塞等待条件变量并释放锁, 当被唤醒时才重新加锁. 在调该函数之前需要线程已加互斥锁, 再判断是否符合条件变量(如共享资源为空)来阻塞并释放锁.
pthread_cond_timedwait(cond, mutex, abstime)
阻塞一定时间就不等了, abstime是一个timespec结构体, 有tv_sec(秒)和tv_nsec(纳秒)两个成员. 绝对时间是相对于1970年1月1日00:00:00来说, 需要先使用time_t cur = time(NULL)
获取当前时间, 再把cur+n的偏移量赋值给结构体成员tv_sec.
pthread_cond_signal(cond)
唤醒至少一个阻塞在条件变量的线程.
pthread_cond_broadcast(cond)
唤醒所有阻塞在条件变量的线程.
生产者生产后条件变量就满足了, 之后使用signal()/broadcast()唤醒阻塞在条件变量的消费者线程, 不过消费者线程在访问共享数据时仍要先获得锁.
信号量
int sem_init(sem_t *sem, int pshared, unsigned int value)
sem_t虽然是结构体, 但可以近似看作是整数. 由于进程和线程都可以使用信号量, pshared用于指定信号量是否可以在进程间共享(一般传0). value是信号量的初值, 当取1时和互斥量相同. 不同于线程系列函数, 失败返回-1并设置errno.
sem_destroy(sem)
销毁信号量.
在用信号量实现生产者消费者模型时, 生产者先sem_wait(&sem_blank)
使共享资源的空格信号量减少, 然后生产, 之后再sem_post(&sem_product)
使产品信号量增加. 对应消费者那边先sem_wait(&product)
使产品信号量减少, 然后消费走, 再sem_post(&sem_blank)
使空格信号量增加.
共享资源的环形队列可以用数组实现, i = (i+1)%NUM, 通过取余实现逻辑上的环.
网友评论