linux多线程编程
基本知识
每个线程的自有数据:
- 线程id
- 一组寄存器值
- 栈
- 调度优先级和策略
- 信号屏蔽字
errno变量(Linux的一个全局变量,用于保存Linux API调用失败的错误码)- 线程私有数据
所有线程的共享数据:
- 可执行程序的代码
- 程序的全局内存
- 堆内存
- 栈?及文件描述符
本文讨论的线程接口来自POSIX.1-2001,线程接口也称为‘pthread’或‘POSIX线程’。
线程库头文件<pthread.h>
线程ID
- 线程id是用
pthread_t这个数据类型表示的。 -
pthread_t类型只是个标准,不同系统的实现不一样。Linux 3.2.0使用无符号长整形,Solaris 10使用无符号整型,FreeBSD 8.0和MAC OS X 10.6.8使用一个指向pthread结构的指针。经过实测,在64位centos7上是8个字节的unsigned long类型。 -
函数
int pthread_equal(pthread_t tid1, pthread_t tid2);用来判断两个线程id是否相同。若相等,返回非0,否则返回0。 - 函数
pthread_t pthread_self(void);返回线程自身的id。
创建线程
int pthread_create(pthread_t* , const pthread_attr_t * , void *(*)(void*), void *)- 参数1:调用后生成一个线程id的指针
- 参数2:指定各种线程属性,为
NULL时采用默认属性。 - 参数3:线程的回调函数,该函数只有一个
void*参数,传递多个参数时需要将这些参数封装到一个结构内。 - 参数4:传递给回调函数的参数的指针(用于给回调函数传递参数)
- 返回值:成功返回0,否则返回错误编号(这个错误编号不同于errno,但是同时每个线程内置了errno,为了与其他函数兼容)
- 新线程继承调用线程的浮点环境信号屏蔽字,但是挂起信号集会被清除。
线程退出
- 进程中的任何一个线程如果调用了
exit、_Exit或_exit,整个进程就会终止。 - 线程的退出方式有三种
- 回调函数正常返回;
- 回调函数通过
void pthread_exit(void *rval_ptr)返回; - 进程中的其他线程终止其执行;
int pthread_join(pthread_t tid, void **rval_ptr),调用这个函数的线程会阻塞,直到指定的线程返回,参数rval_ptr指向的就是pthread_exit和线程回调函数返回的结果,如果线程被其他线程终止,rval_ptr就指向PTHREAD_CANCELED。当然线程回调函数也可以返回NULL。有了pthread_join,线程就可以返回结构体了。注意,pthread_join的第二个参数一定要事先定义成void*,然后用&传参,如果定义成void**会导致无法通过这个参数拿到线程返回的结构(这是个语法问题)- 线程可以调用
pthread_cancel取消其他线程的执行,但这个函数只是发出一个请求,被请求的线程可以忽略cancel或者自己决定怎么cancel。线程被cancel之后,它返回的void*指针或rval_ptr*这个指针会被置为-1(即PTHREAD_CANCELED),也就是说此时我们无法从pthread_join获得的返回值中取出任何东西
线程退出的其他内容
- 线程的分离状态:线程处于分离状态时,其资源才会被释放,可以调用
pthread_detach使线程处于分离状态,此时调用pthread_join会失败,返回EINVAL。pthread_join的作用是让一个未分离的线程进入分离状态并释放其资源。 - 线程清理处理程序(thread cleanup handler):线程可以选择在退出时调用清理程序,一个线程可以执行多个清理程序。(略)
线程同步
互斥量(mutex)
- 拿不到锁的线程会阻塞
- 数据类型:
pthread_mutex_t - 创建和销毁:
PTHREAD_MUTEX_INITIALIZER、pthread_mutex_init、pthread_mutex_destroy - 静态创建使用
PTHREAD_MUTEX_INITIALIZER,这里的静态指的是初始化时创建mudex(即定义时立即指定mutex的值),静态创建的mutex不需要destroy。 - 上锁和解锁:
pthread_mutex_lock、pthread_mutex_trylock、pthread_mutex_unlock,trylock可以让线程在不阻塞的情况下不断地试图获取锁。 - 互斥量避免死锁:两个资源分别利用两个不同的互斥量加锁,两个线程分别持有一个资源,又去试图获取另外一个锁。解决办法:
- 规定线程上锁的顺序;
- 使用trylock,如果发现没有获得锁,先把其他锁释放掉。
pthread_mutex_timedlock,设定线程阻塞时间,如果在超时后还没有拿到锁,返回错误。这个函数可以防止线程被永久阻塞。
读写锁
- 三种状态:读锁、写锁、不加锁。
- 读写锁适合多读少写的场景。
- 读写锁如何避免读锁长时间被占用:当读写锁处于读状态时,如果有线程试图获取写锁时,读写锁会阻塞后面的读请求。
- 数据类型:
pthread_rwlock_t - 创建和销毁:
pthread_rwlock_init、pthread_rwlock_destroy、PTHREAD_RWLOCK_INITIALIZER - 加锁和解锁:
pthread_rwlock_rdlock、pthread_rwlock_wrlock、pthread_rwlock_unlock - 非阻塞版:
pthread_rwlock_tryrdlock、pthread_rwlock_trywrlock - 超时版:
pthread_rwlock_timedrdlock、pthread_rwlock_timedwrlock
条件变量
- 条件变量和互斥量一起使用,条件变量改变状态时要用互斥量加锁。
- 数据类型:
pthread_cond_t - 创建和销毁:
pthread_cond_init、pthread_cond_destroy、PTHREAD_COND_INITIALIZER int pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*):该函数会将调用线程放到条件变量的等待队列上,调用线程阻塞等待。函数对变量修改完成后解锁mutex,但是返回时会再次对mutex上锁- 超时版:
pthread_cond_timedwait - 通知条件满足:
pthread_cond_signal(pthread_cond_t*)唤醒一个等待的线程,pthread_cond_broadcast(pthread_cond_t)唤醒所有线程。
条件变量为什么一定要和mutex一起使用
wait函数的执行过程分为三步,(1)检查条件是否为真;(2)如果不为真,则线程加入等待队列并进入阻塞;(3)接收到条件变化的信号,重新检查条件并唤醒线程;
如果没有mutex,可能会在(1)(2)两步之间发生了条件的变化,但是线程已经准备进入阻塞,有可能再也无法被唤醒。
条件变量的虚假唤醒
以典型的生产者消费者场景为例,消费者在调用wait之前要检查消息队列是否为空,如果消息队列为空,则调用wait等待生产者的信号。
如果检查队列是否为空是用if判断的,则有可能会发生这种情况:当wait函数接收到信号准备重新获取mutex并唤醒线程之前,另一个消费者抢先获取到了mutex并把新的消息给消费掉了,这种情况就属于虚假唤醒。这时前一个等待的线程即使被唤醒了也拿不到新的消息,程序可能会因为无效的内存访问而崩溃。
因此,调用wait前对队列是否为空的判断一定要用while循环。
自旋锁
- 自旋锁是一个与mutex类似的互斥的锁,但是线程拿不到锁时,不会进入阻塞状态,而是处于忙等待的状态,也就是说,等待自旋锁的线程一直在占用cpu资源。
- 自旋锁适合锁被持有的时间非常短的情况,这种情况下线程自旋等待的资源消耗小于线程调度的消耗。
自旋锁在用户层不太有用,(1)在分时调度系统中(系统分为实时调度和分时调度两种),当一个拥有自旋锁的线程被抢占后,这个线程只能进入阻塞,但是其他等待自旋锁的线程就会持续占用cpu资源。(2)现代处理器的上下文切换速度越来越快,并且很多互斥量的实现也非常高效,例如有些互斥量会在刚开始等待时让线程自旋一小段时间。
- 接口:
pthread_spin_init、pthread_spin_destroy、pthread_spin_lock、pthread_spin_trylock、pthread_spin_unlock
屏障(barrier)
- 屏障让线程执行到屏障时等待,直到所有线程都执行到屏障,才会接着工作。
- 数据类型:
pthread_barrier_t - 创建和销毁:
pthread_barrier_init(*barrier, *attr, count),使用count指定必须执行到屏障的数量。pthread_barrier_destroy - 等待屏障:
pthread_barrier_wait,所有参加屏障功能的线程必须都调用这个函数,如果到达屏障的线程小于count时,先到达屏障的线程要阻塞等待。