pthread用法总结
Linux下编写多线程程序需要包含头文件pthread.h
线程管理
线程id
typedef unsigned long int pthread_t;
获取自身线程id:
pthread_t pthread_self();
线程属性
// 初始化属性
int pthread_attr_init(pthread_attr_t *attr)
// 删除线程属性
pthread_attr_destroy(pthread_attr_t *attr)
通过pthread_attr_get*
和pthread_attr_set*
函数可以获取和设置下面的线程属性:
typedef struct {
int detachstate; // 线程的分离状态
int schedpolicy; // 线程调度策略
struct sched_param schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区大小
void* stackaddr; // 线程栈的位置
size_t stacksize; // 线程栈的大小
} pthread_attr_t;
主要介绍以下几个属性:
- detachstate: 该属性决定了线程执行任务后以什么方式来结束自己
- PTHREAD_CREATE_JOINABLE(默认)
- PTHREAD_CREATE_DETACHED
- schedpolicy: 该属性决定了线程的调度策略
- SCHED_OTHER(默认):优先级为0,一种基于时间片的分时调度。
- SCHED_FIFO:一种实时调度策略。一旦一个SCHED_FIFO线程获得CPU,它会一直运行,直到它完成任务、被更高优先级的线程抢占或主动放弃 CPU。相同优先级的SCHED_FIFO线程会按先进先出的顺序执行。
- SCHED_RR:也是一种实时调度策略,类似于SCHED_FIFO。不同之处在于,SCHED_RR线程会获得一个固定的时间片。当一个SCHED_RR线程的时间片用完后,即使它没有完成任务,也会被调度器中断,并被放到其优先级队列的末尾,等待下一次运行。
- schedparam:是一个整数值,用于在特定的调度策略中定义线程的相对优先级。
线程创建
// thread 线程标识符
// attr 线程属性,无需修改传NULL
// start_routine 线程函数
// arg 线程函数的参数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
线程汇合
pthread_join()函数会一直阻塞调用线程,直到指定的线程结束。
指定的线程必须是joinable的,对一个线程重复调用pthread_join()会导致未定义行为。
// tid 等待退出的线程id
// rval_ptr 用户定义的指针,用来存储被等待线程结束时的返回值(当该参数不为NULL时)
int pthread_join(pthread_t thread, void ** rval_ptr);
线程分离
int pthread_detach(pthread_t thread)
线程终止
// retval 由用户指定的参数,pthread_exit完成之后可以通过这个参数获得线程的退出状态
void pthread_exit (void *retval);
// 请求取消同一进程中的线程,被取消的线程由thread参数指定,操作成功则返回0
int pthread_cancel (pthread_t thread);
互斥锁
线程的互斥
线程间的互斥是为了避免对共享资源或临界资源的同时使用,从而避免因此而产生的不可预料的后果。临界资源一次只能被一个线程使用。线程互斥关系是由于对共享资源的竞争而产生的间接制约。
互斥锁用来保证一段时间内只有一个线程在执行一段代码,实现了对一个共享资源的访问进行排队等候。互斥锁是通过互斥锁变量来对访问共享资源排队访问。
互斥量是pthread_mutex_t
类型的变量。互斥量有两种状态:lock(上锁)、unlock(解锁)。
当对一个互斥量加锁后,其他任何试图访问互斥量的线程都会被堵塞,直到当前线程释放互斥锁上的锁。如果释放互斥量上的锁后,有多个堵塞线程,这些线程只能按一定的顺序得到互斥量的访问权限,完成对共享资源的访问后,要对互斥量进行解锁,否则其他线程将一直处于阻塞状态。
// 初始化
int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);
// 上锁
int pthread_mutex_lock(pthread_mutex_t * mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t * mutex);
// 判断是否上锁
// 0表示已上锁,非0表示未上锁
int pthread_mutex_trylock(pthread_mutex_t * mutex);
// 销毁
int pthread_mutex_destory(pthread_mutex_t * mutex);
自旋锁
自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于:当自旋锁尝试获取锁时以忙等待的形式不断地循环检查锁是否可用。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
自旋锁和互斥锁的区别
从实现原理上来讲,互斥锁属于sleep-waiting类型的锁,而自旋锁属于busy-waiting类型的锁。也就是说:
pthread_mutex_lock()
操作,如果没有锁成功的话就会调用system_wait()
的系统调用并将当前线程加入该互斥锁的等待队列里;pthread_spin_lock()
可以理解为在一个while(1)
循环中用内嵌的汇编代码实现的锁操作。
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间;
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。
// 初始化
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// 上锁
int pthread_spin_lock(pthread_spinlock_t *lock);
// 解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
// 判断是否上锁
int pthread_spin_trylock(pthread_spinlock_t *lock);
// 销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
条件变量
条件变量的功能是阻塞线程,直至接收到条件成立的信号后,被阻塞的线程才能继续执行。
一个条件变量可以阻塞多个线程,这些线程会组成一个等待队列。当条件成立时,条件变量可以解除线程的被阻塞状态。也就是说,条件变量可以完成以下操作:
- 阻塞线程,直至接收到条件成立的信号;
- 向等待队列中的一个或所有线程发送条件成立的信号,解除它们的被阻塞状态。
为了避免多线程之间发生“抢夺资源”的问题,条件变量在使用过程中必须和一个互斥锁搭配使用。
// 初始化
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr);
// 阻塞当前线程,等待条件成立
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
// 在abstime参数指定的时间内阻塞线程,超出时限后,该函数将重新对互斥锁执行加锁操作,并解除对线程的阻塞
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
// 解除线程的阻塞状态
// 函数至少解除一个线程的被阻塞状态,如果等待队列中包含多个线程,优先解除哪个线程将由操作系统的线程调度程序决定
int pthread_cond_signal(pthread_cond_t* cond);
// 解除等待队列中所有线程的被阻塞状态
int pthread_cond_broadcast(pthread_cond_t* cond);
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
详细说明pthread_cond_wait的下工作原理:
- 解锁mutex,并将当前线程加入到cond的等待队列中,使其进入阻塞状态(这个过程是原子性的)
- 一直保持阻塞状态,直到另一个线程唤醒它
- 当线程被唤醒后,尝试重新获取mutex
- 直到成功获取到mutex,函数才返回
信号量
和互斥锁类似,信号量本质也是一个全局变量。不同之处在于,互斥锁的值只有 2 个(加锁 “lock” 和解锁 “unlock”),而信号量的值可以根据实际场景的需要自行设置(取值范围为 ≥0)。更重要的是,信号量还支持做“加 1”或者 “减 1”运算,且修改值的过程以“原子操作”的方式实现。
原子操作是指当多个线程试图修改同一个信号量的值时,各线程修改值的过程不会互相干扰。例如信号量的初始值为 1,此时有 2 个线程试图对信号量做“加 1”操作,则信号量的值最终一定是 3,而不会是其它的值。反之若不以“原子操作”方式修改信号量的值,那么最终的计算结果还可能是 2(两个线程同时读取到的值为 1,各自在其基础上加 1,得到的结果即为 2)。
多线程程序中,使用信号量需遵守以下几条规则:
- 信号量的值不能小于 0;
- 有线程访问资源时,信号量执行“减 1”操作,访问完成后再执行“加 1”操作;
- 当信号量的值为 0 时,想访问资源的线程必须等待,直至信号量的值大于 0,等待的线程才能开始访问。
根据初始值的不同,信号量可以细分为 2 类,分别为二进制信号量和计数信号量:
- 二进制信号量:指初始值为 1 的信号量,此类信号量只有 1 和 0 两个值,通常用来替代互斥锁实现线程同步;
- 计数信号量:指初始值大于 1 的信号量,当进程中存在多个线程,但某公共资源允许同时访问的线程数量是有限的(出现了“狼多肉少”的情况),这时就可以用计数信号量来限制同时访问资源的线程数量。
// 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 将信号量的值加1,同时唤醒其它等待访问资源的线程
int sem_post(sem_t* sem);
// 当信号量的值大于0,sem_wait() 会对信号量做减1操作
// 当信号量的值为0时,sem_wait() 会阻塞当前线程,直至有线程执行sem_post(),暂停的线程才会继续执行;
int sem_wait(sem_t* sem);
// sem_trywait() 和 sem_wait() 类似,唯一的不同在于,当信号量的值为 0 时,sem_trywait() 并不会阻塞当前线程,而是立即返回 -1
int sem_trywait(sem_t* sem);
// 销毁
int sem_destroy(sem_t* sem);