Skip to content

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会失败,返回EINVALpthread_join的作用是让一个未分离的线程进入分离状态并释放其资源。
  • 线程清理处理程序(thread cleanup handler):线程可以选择在退出时调用清理程序,一个线程可以执行多个清理程序。(

线程同步

互斥量(mutex)

  • 拿不到锁的线程会阻塞
  • 数据类型:pthread_mutex_t
  • 创建和销毁:PTHREAD_MUTEX_INITIALIZERpthread_mutex_initpthread_mutex_destroy
  • 静态创建使用PTHREAD_MUTEX_INITIALIZER,这里的静态指的是初始化时创建mudex(即定义时立即指定mutex的值),静态创建的mutex不需要destroy。
  • 上锁和解锁:pthread_mutex_lockpthread_mutex_trylockpthread_mutex_unlock,trylock可以让线程在不阻塞的情况下不断地试图获取锁。
  • 互斥量避免死锁:两个资源分别利用两个不同的互斥量加锁,两个线程分别持有一个资源,又去试图获取另外一个锁。解决办法:
  • 规定线程上锁的顺序;
  • 使用trylock,如果发现没有获得锁,先把其他锁释放掉。
  • pthread_mutex_timedlock,设定线程阻塞时间,如果在超时后还没有拿到锁,返回错误。这个函数可以防止线程被永久阻塞

读写锁

  • 三种状态:读锁、写锁、不加锁。
  • 读写锁适合多读少写的场景。
  • 读写锁如何避免读锁长时间被占用:当读写锁处于读状态时,如果有线程试图获取写锁时,读写锁会阻塞后面的读请求。
  • 数据类型:pthread_rwlock_t
  • 创建和销毁:pthread_rwlock_initpthread_rwlock_destroyPTHREAD_RWLOCK_INITIALIZER
  • 加锁和解锁:pthread_rwlock_rdlockpthread_rwlock_wrlockpthread_rwlock_unlock
  • 非阻塞版:pthread_rwlock_tryrdlockpthread_rwlock_trywrlock
  • 超时版:pthread_rwlock_timedrdlockpthread_rwlock_timedwrlock

条件变量

  • 条件变量和互斥量一起使用,条件变量改变状态时要用互斥量加锁。
  • 数据类型:pthread_cond_t
  • 创建和销毁:pthread_cond_initpthread_cond_destroyPTHREAD_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_initpthread_spin_destroypthread_spin_lockpthread_spin_trylockpthread_spin_unlock

屏障(barrier)

  • 屏障让线程执行到屏障时等待,直到所有线程都执行到屏障,才会接着工作。
  • 数据类型:pthread_barrier_t
  • 创建和销毁:pthread_barrier_init(*barrier, *attr, count),使用count指定必须执行到屏障的数量。pthread_barrier_destroy
  • 等待屏障:pthread_barrier_wait,所有参加屏障功能的线程必须都调用这个函数,如果到达屏障的线程小于count时,先到达屏障的线程要阻塞等待。