APUE第11章 线程


以下说的都是pthread的相关内容,C++对应的是std::thread

线程标识

每个线程也有一个线程ID,但是只有在所属进程的上下文才有意义

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//若相等返回非0,否则返回0
pthread_t pthread_self(void);
//返回线程的线程ID

创建线程

#include <pthread.h>
int pthread_create(pthread_ *restrict tidp, const pthread_attr_t, void *(*start_rtn)(void *), void *restrict arg);
//返回值若成功返回0,否则返回错误编号

参数说明

  1. tidp在线程创建成功后会指向创建好的线程
  2. start_rtn是执行的函数,只接受一个参数arg,如果要接受多个的话要放在一个结构体里面
  3. 线程创建的时候并不能保证哪个先运行

线程终止

如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程都会终止。同样的,如果发送给线程的信号默认动作是终止的话,这个信号就会终止整个进程。

线程的三种退出方式

  1. 线程可以简单的从启动例程中返回,返回值是线程的退出码,
  2. 线程可以被统一进程中的其他进程取消
  3. 线程调用pthread_exit
    #include <pthread.h>
    void pthread_eixt(void **rval_ptr);
    

pthread_join

作用是等待指定线程调用pthread_exit、从启动例程返回或者被取消。如果简单返回,rval_ptr就包含返回码。如果被取消,则rval_ptr制定的内存单元就设置为PTHREAD_CANCELED

#include <pthread.h>
int pthread_join(thread_t thread, void **rval_ptr);
//返回值,若成功就返回0,否则返回错误编号

pthread_cancel

用于取消线程

#include <pthread.h>
int pthread_cancel(pthread_t tid);

pthread_cancel并不等于直接终止,它会调用他注册的退出函数,就像atexit函数一样。

pthread_cleanup_push和pthread_cleanup_pop

pthread_cleanup_push和atexit类似,都是注册相关的清理函数,而pthread_cleanup_pop则是取消上次push的函数(当execute参数为0的时候),按照现象来讲像是一个栈,且执行顺序和push的顺序相反

#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

arg参数对应的是:

  1. pthread_exit调用时的
  2. 响应请求取消的时候
  3. 用非零execute的调用pthread_cleanup_pop的时候

线程同步

如果多个线程对同一个资源可以同时读写的话,可能会导致访问到无效的数据,这是由于非原子操作产生的,非原子操作的原因跟处理器体系结构有关,可移植程序并不能对处理器体系结构作出任何假设,所以需要线程同步。

互斥量

mutex实质上是一把锁,保证同一时刻只有一个访问者(线程)访问数据,其他试图加锁的线程将被阻塞直到当前线程释放锁。如果某个线程不需要得到锁也能修改数据,那么就算剩下的线程都使用锁,一样会出现数据不一致的问题。

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const int pthread_mutexattr_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);
//若要用默认属性初始化互斥量,知足要把attr设为NULL即可

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//以上的函数成功返回0,否则返回错误编号

如果线程不希望被阻塞,可以使用trylock,这是个原子操作,尝试锁住锁,如果成功则立即返回0,否则立即出错返回EBUSY,感觉可以用这个实现自旋锁

死锁

死锁四个条件:循环等待,互斥,战友并等待,不可抢占

pthread_mutex_timedlock

这是一个比较有用的函数,在pthread_mutex_lock加锁的时候会一直阻塞,而使用这个函数的时候,可以指定超时的时间,超时的话返回ETIMEOUT

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
//timespec可以用秒和纳秒来描述时间
//clock_gettime(CLOCK_REALTIME, &timespec);

Note: 时间可能不会完全精确,原因是系统的最小计时周期比较大,导致精度不足。

读写锁

互斥量直接对任意两个想要访问资源的个体进行排斥,并行性较低,读写锁支持更高程度的并行性。

三种读写锁状态

  1. 读模式下加锁:所有试图用读模式加锁的请求都会成功,但是如果以写模式加锁的话就会被阻塞,直到所有读锁释放。一般来讲当写锁请求的时候,会阻塞后来的读锁请求,以免饿死。
  2. 写模式下加锁:所有试图对这个所加锁的请求都会被阻塞
  3. 不加锁状态:读写锁请求不会阻塞

    与互斥量比起来,读写锁使用前必须初始化,释放底层内存前必须销毁

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//成功返回0,否则返回错误编号
//同样的如果希望该锁有默认属性,attr传入NULL即可,或者直接初始化为PTHREAD_RWLOCK_INITIALIZER常量即可

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//返回0代表成功,否则返回错误编号

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//返回0或者错误码如EBUSY

#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const stuct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timesppect *restrict tsptr);
//成功返回0,否则返回错误编号

Note:由于实现可能会对共享模式下可获取的读写锁的写模式次数进行限制,所以调用函数的时候应当检查返回值。

条件变量

条件变量本身由互斥量保护。线程在改变条件的时候应当锁住互斥量

#include <ptherad.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
//成功返回0,错误返回失败码

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *mutex, const struct timespec *restrict tsptr);

//唤醒一个等待的线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒所有等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);

自旋锁

一直忙等,适用于锁的时间很短,线程切换和调度成本高于忙等。线程如果已经对其加了自旋锁,如果再加的话结果未定义。有可能会产生死锁。

屏障

barrier允许每个线程等待,直到所有合作线程都到达某一点,才继续从这个点继续执行。