1. 线程模型

根据运行环境和调度身份, 线程分为 内核线程和用户线程.
内核线程运行在内核空间, 由内核调度
用户线程运行在用户空间, 由线程库调度
内核线程获得CPU的使用权时, 它加载并运行一个用户线程, 一个进程可以拥有M个内核线程和N个用户线程(M <= N). 按照M:N的取值, 线程的实现方式分为三种:

  1. 完全用户空间实现
  2. 完全内核调度
  3. 双层调度

2. Linux线程库

Linux的线程库采用1:1的方式实现, 即一个内核线程对应一个用户线程, 现代Linux上默认采用的线程库是NPTL
新的NPTL库优势:

  1. 内核线程不再是一个进程, 避免了进程模拟线程导致的语义问题
  2. 摒弃了管理线程, 终止线程和回收线程堆栈由内核完成
  3. 一个进程的线程可以运行在不同CPU上, 充分利用多核优势
  4. 线程同步由内核完成, 不同进出间的线程可以共享互斥锁

3. 创建线程和结束线程

#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);复制代码

thread: 新线程的标识符. typedef unsigned long int pthread_t;
attr: 线程属性, NULL表示默认属性
start_routine和arg表示新线程运行的函数及参数
成功返回0, 失败返回错误码, 用户打开的线程数量不能超过RLIMIT_NPROC软资源限制
所有用户创建的总线程数也不能超过/proc/sys/kernel/threads_max内核参数定义的值

void pthread_exit(void* retval);复制代码

线程函数结束时最好调用, 以确保安全干净的退出
retval参数向线程回收者传递退出信息

int pthread_join(pthread_t thread, void** retval);复制代码

进程中的任意线程都可以调用pthead_join来回收其它线程(目标线程必须可回收), 即等待目标线程结束, 调用线程会一直阻塞, 直到目标线程结束
成功返回0 失败返回错误码, 可能的错误码如下:
EDEADLK: 可能引起死锁
EINVAL: 目标线程不可回收
ESRCH: 目标线程不存在

int pthread_cancel(pthread_t thread);复制代码

成功返回0, 失败返回错误码, 目标线程可以决定是否允许取消以及如何取消, 由如下两个函数实现:

int pthread_setcancelstate(int state, int* oldstate);
int pthread_setcanceltype(int type, int* oldtype);复制代码

state: 取消状态, 可选值有:
PTHREAD_CANCEL_ENABLE: 允许线程被取消, 线程创建时的默认取消状态
PTHREAD_CANCEL_DISABLE: 禁止线程被取消, 如果一个线程收到取消请求, 则请求被挂起, 直到目标线程设置为允许取消

type: 取消类型, 可选值有:
PTHREAD_CANCEL_ASYNCHRONOUS: 线程收到取消请求立刻取消
PTHREAD_CANCEL_DEFERRED: 延迟取消, 直到调用了下面所谓的取消点函数: pthread_join, pthread_testcancel, pthread_cond_wait, pthread_cond_timewait, sem_wait和sig_wait, 建议调用pthread_testcancel来设置取消
成功返回0, 失败返回错误码

4. 线程属性

未完待续...

5. 信号量

#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);复制代码

初始化一个未命名的信号量, pshared指定信号量类型, 0表示进程的局部信号量, 否则此信号量可以在进程间共享, value表示信号量的初始值, 初始化一个已经初始化的信号量产生无法预期的结果

int sem_destroy(sem_t* sem);复制代码

销毁一个信号量, 释放其占用的内核资源, 销毁一个被其它线程等待的信号量, 将导致不可预期的结果

int sem_wait(sem_t* sem);复制代码

原子操作的方式将信号量的值减1, 如果信号量为0, 则调用将阻塞, 直到信号量具有非0值

int sem_trywait(sem_t* sem);复制代码

信号量为非0时, 对信号量减1. 当信号量为0时, 返回-1并设置errno为EAGAIN. sem_wait的非阻塞版本

int sem_post(sem_t* sem);复制代码

原子操作的方式将信号量加1, 大于0时, 调用sem_wait等待信号量的线程将被唤醒

所有的操作成功返回0, 失败返回-1并设置errno

6. 互斥锁

用来保护关键代码段, 确保独占式的访问, 进入代码段时, 获得互斥锁并加锁, 等于信号量的wait操作, 离开代码段时, 对互斥量解锁, 唤醒其它等待该互斥锁的线程, 相当于信号量的post操作

#include <pthead.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);复制代码

mutex指向目标互斥锁, 此函数初始化互斥锁, mutexattr指定互斥锁的属性, 如果为NULL, 表示默认属性

// 另一种初始方式, 锁的各个字段都被初始化为0
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;复制代码
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);复制代码

以原子的方式对互斥体加解锁, trylock是非阻塞版本, 对一个已经加锁的互斥锁加锁, 会返回错误码EBUSY
所有的操作成功返回0, 失败返回错误码

7. 互斥锁属性

未完待续...

8. 死锁举例

对已经加锁的普通锁再次加锁(递归调用), 导致死锁
两个线程按照不同顺序申请两个互斥锁, 导致死锁

9. 条件变量

互斥锁用于同步线程共享数据的访问, 条件变量则用于同步共享数据的值, 它提供了线程间的通知机制: 某个共享数据达到某个值, 唤醒等待这个共享数据的线程

#include <pthread.h>
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);复制代码

cond指向目标条件变量, 初始化条件变量, cond_attr指定条件变量的属性, 如果为NULL表示默认属性

// 另一种方式初始化条件变量, 各个字段被设置为0
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;复制代码
int pthread_cond_destroy(pthread_cond_t* cond);复制代码

销毁条件变量, 释放占用的内核资源, 销毁一个正在等待的条件变量将失败并返回EBUSY

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);复制代码

等待目标条件变量, mutex用于保护条件变量的互斥锁, 保证wait操作的原子性
调用wait前要保证当前线程持有mutex并已经加锁, 否则导致不可预期的结果
wait执行时将调用线程放入等待条件变量的等待队列, 然后解锁mutex. 在这个过程中, broadcast和signal不会修改条件变量, wait函数不会错过目标条件变量的任何变化, wait函数成功返回时, mutex将被重新上锁

所有函数成功返回0, 失败返回错误码

10. 线程同步机制封装

未完待续...

11. 可重入函数

如果一个函数被多个线程调用且不会发生竞态条件, 则称其为线程安全的或者可重入函数
Linux库函数只有一小部分不可重入, 并提供了其可重入版本, 即在函数名后加上_r, 比如: localtime对应的可重入版本localtime_r

12. 线程和进程

多线程程序调用fork, 子进程只拥有一个执行线程, 它是父进程中调用fork的线程的完整复制, 并且子进程会自动继承父进程中互斥锁, 条件变量的状态

13.线程和信号

每个线程可以独立设置新号掩码, 多线程环境下的设置函数:

#include <pthread.h>
#include <signal.h>
int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);复制代码

成功返回0, 失败返回错误码
线程共享进程的新号, 线程库将根据线程掩码决定把新号发给具体的线程. 如果每个线程单独设置信号掩码, 将导致逻辑错误.
所有线程共享新号处理函数, 当一个线程设置了某个信号的处理函数, 它将覆盖其它线程同一个信号的处理函数, 所以我们应该定义一个专门的线程处理所有信号, 步骤如下:

  1. 主线程创建其它子线程之前调用pthread_sigmask设置好信号掩码, 新创建的子线程将自动继承信号掩码, 这样所有的线程都不会响应被屏蔽的信号
  2. 在某个线程调用如下函数等待信号并处理:
#include <signal.h>
int sigwait(const sigset_t* set, int* sig);复制代码

set: 需要等待的信号集合, 可以将其指定为步骤1中的信号掩码, 表示在此线程中等待所有被屏蔽的信号
sig: 存储函数返回的信号值
sigwait成功返回0, 失败返回错误码

int pthread_kill(pthread_t thread, int sig);复制代码

明确发送一个信号给指定的线程, thread: 目标线程, sig: 待发送的信号
sig若为0, 函数不会发送信号, 但仍会执行错误检查, 利用这种方法来检测目标程序是否存在
成功返回0, 失败返回错误码