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;

主要介绍以下几个属性:

  1. detachstate: 该属性决定了线程执行任务后以什么方式来结束自己
    • PTHREAD_CREATE_JOINABLE(默认)
    • PTHREAD_CREATE_DETACHED
  2. schedpolicy: 该属性决定了线程的调度策略
    • SCHED_OTHER(默认):优先级为0,一种基于时间片的分时调度。
    • SCHED_FIFO:一种实时调度策略。一旦一个SCHED_FIFO线程获得CPU,它会一直运行,直到它完成任务、被更高优先级的线程抢占或主动放弃 CPU。相同优先级的SCHED_FIFO线程会按先进先出的顺序执行。
    • SCHED_RR:也是一种实时调度策略,类似于SCHED_FIFO。不同之处在于,SCHED_RR线程会获得一个固定的时间片。当一个SCHED_RR线程的时间片用完后,即使它没有完成任务,也会被调度器中断,并被放到其优先级队列的末尾,等待下一次运行。
  3. 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的下工作原理:

  1. 解锁mutex,并将当前线程加入到cond的等待队列中,使其进入阻塞状态(这个过程是原子性的)
  2. 一直保持阻塞状态,直到另一个线程唤醒它
  3. 当线程被唤醒后,尝试重新获取mutex
  4. 直到成功获取到mutex,函数才返回

信号量

和互斥锁类似,信号量本质也是一个全局变量。不同之处在于,互斥锁的值只有 2 个(加锁 “lock” 和解锁 “unlock”),而信号量的值可以根据实际场景的需要自行设置(取值范围为 ≥0)。更重要的是,信号量还支持做“加 1”或者 “减 1”运算,且修改值的过程以“原子操作”的方式实现。

原子操作是指当多个线程试图修改同一个信号量的值时,各线程修改值的过程不会互相干扰。例如信号量的初始值为 1,此时有 2 个线程试图对信号量做“加 1”操作,则信号量的值最终一定是 3,而不会是其它的值。反之若不以“原子操作”方式修改信号量的值,那么最终的计算结果还可能是 2(两个线程同时读取到的值为 1,各自在其基础上加 1,得到的结果即为 2)。

多线程程序中,使用信号量需遵守以下几条规则:

  1. 信号量的值不能小于 0;
  2. 有线程访问资源时,信号量执行“减 1”操作,访问完成后再执行“加 1”操作;
  3. 当信号量的值为 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);