Pthreads即POSIX标准线程。线程共享进程资源,包括程序text正文段,进程的全局变量、堆、栈和文件描述符等。在一个进程内,线程由代表可执行上下文的必要信息组成:线程ID,一系列寄存器值,栈,调度优先级,信号掩码等。
线程ID
线程ID只在同一进程内具有唯一性。线程ID用pthread_t数据类型表示,Linux实际为无符号长整型,而Mac OSX用的是指向pthread结构体的指针,所以比较线程ID得用pthread_equal方法。
#include <pthread.h>
pthread_t pthread_self(void); //获取当前线程ID
int pthread_equal(pthread_t tid1, pthread_t tid2);//比较线程ID
线程创建
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void), void *restrict arg);
- tidp指向返回的线程ID。
- attr自定义线程属性下章介绍,先使用NULL。
- start_rtn新建线程的启动方法,arg为传递给start_rtn的参数。
- pthread库函数通常返回错误码,而不是像其他函数一样修改errno,保留error(每个线程一份拷贝)目的只是向后兼容。
线程终止
退出线程的方法有以下几种:
- 线程正常退出。
- 被同一进程中别的线程取消(cancel)。
- 调用pthread_exit函数。
- 线程调用exit,_exit函数会导致整个进程退出。
信号发送给线程,如果默认动作是终止进程,也会终止整个进程。
#include <pthread.h> void pthread_exit(void *rval_ptr); int thread_join(pthread_t thread, void **rval_ptr); int pthread_cancel(pthread_t tid); void pthread_cleanup_push(void (*rtn)(void *), void *arg);
pthread_exit函数中rval_ptr参数是无类型指针,和传递给上文中创建线程start_rtn启动函数的arg参数类似。如果调用thread_join函数,线程的返回值会被用到,所以rval_ptr不能是局部变量,否则线程结束时内存会被回收导致错误。
- pthread_join函数用于阻塞当前线程,直到thread参数代表的线程(处于detached状态)终止,rval_ptr参数指向其返回值。
- pthread_cancel用于取消指定线程,它只是发出取消请求,立即返回。
- pthread_cleanup_push和进程的atexit函数类型,注册线程终止前的清理函数(pthread_exit和cancel时都清理函数都会被调用)
线程同步
Mutexes互斥锁
互斥锁的使用方法比较简单,在线程访问共享资源之前加锁,完成之后释放锁,互斥锁会保证同一时间只有一个线程能够加锁成功,其他线程会一直阻塞到锁被释放。
pthread_mutex_lock(pthread_mutex_t *mutex);
……//访问共享资源
pthread_mutex_unlock(pthread_mutex_t *mutex);
死锁
常见的一种死锁场景发生于各线程试图获取多个锁,而且锁定顺序不一致时。比如线程A先获取锁1,然后试图获取锁2;而同时线程B已经获取锁2,然后试图获取锁1,双方都被阻塞导致锁无法被释放。所以通过精心安排各线程获取锁的顺序可以避免锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
这两个函数都能有效避免死锁:
- trylock在mutex已经上锁的情况下不会阻塞,会立即返回EBUSY。
- timedlock超时会返回ETIMEDOUT
Reader-Writer Locks读写锁
也被称之为共享-排他锁。允许多个线程获取读锁,而只能有一个线程持有写锁;而且读锁只会阻塞写锁,写锁阻塞其他所有锁,所以更适用于读多写少的场景。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//获取读锁,同时也有tryrdlock和timedlock版本
int pthread_rwlock_rwlock(pthread_rwlock_t *rwlock);//获取写锁,同时也有tryrwlock和timedlock版本
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//释放锁的接口是一致的
Condition Variables条件变量
条件变量和互斥锁一起配合使用,不满足条件时放弃锁等待合适的条件,能够减少资源竞争。条件变量本身被互斥锁“保护”,所以改变条件变量状态之前首先得获取互斥锁。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//有超时版本
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- 线程获取互斥锁之后,如果某个条件不满足,就调用wait等待,这时会释放锁,阻塞。
- 另一个线程用signal通知wait的某个线程;broadcast会通知所有wait的线程。
- 阻塞的线程被唤醒,会重试尝试获取锁,wait返回(也可能获取锁失败,继续阻塞)
Spin Locks自旋锁
自旋锁和互斥锁类似,但是没有获取到锁的线程不会sleep,而是通过busy-wait-loop的方式来获取锁,所以要求使用自旋锁的临界区尽量简短,锁会被尽快释放掉,否则会导致cpu占用飙升。自旋锁通常作为较低层次的原语来实现其他类型锁(实际上,某些互斥锁的实现也会自旋一定时间来获取锁)
Barriers屏障
屏障允许一组线程互相等待,直到所有线程到达某个公共屏障点,然后继续往下运行。和join有点类似,但更强大:允许任意多个线程加入,也不用等到线程退出。常用于将一个大任务拆解为多个子任务由多个线程并发完成,然后主线程继续执行合并处理任务。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);