本文讨论的线程相关的内容都属于POSIX线程(简称pthread)标准,线程库是NPTL(Native POSIX Thread Library),以下的具体包括:
- 创建线程和结束线程
- 读取和设置线程属性
- POSIX线程同步方式:POSIX信号量、互斥锁和条件变量
Linux线程概述
线程模型
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据运行环境和调度者的身份,线程可分为内核线程和用户线程。内核线程,有的系统上称为“轻量级线程”,运行在内核空间,由内核来调度;用户线程运行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程。一个进程可以拥有M个内核线程和N个用户线程,其中M≤N。按照M:N的比值,线程的实现方式可分为三种模式:完全在用户空间实现(1:N)、完全由内核调度(1:1)和双层调度。
Linux线程库
Linux上两个最有名的线程库是LinuxThreads和NPTL,它们都是采用1:1的方式实现的。现在Linux上默认使用的线程库是NPTL。NPTL的主要优势在于:
- 内核线程不再是一个进程
- 摒弃了管理线程,终止线程,回收线程堆栈等工作都可以由内核来完成
- 一个进程的线程可以运行在不同的CPU上,从而充分利用了多处理器系统的优势
- 线程的同步由内核来完成。
创建线程和结束线程
-
pthread_create
创建一个线程的函数
#include <pthread.h> int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
其中
thread
参数是新线程的标识符,attr
参数用于设置新线程的属性,strat_routine
和arg
参数分别指定新线程将运行的函数及其参数。 -
pthread_exit
线程一旦被创建好,内核就可以调度内核线程来执行
start_routine
函数指针所指向的函数了。线程函数在结束时最好调用如下函数,以确保安全、干净地退出:#include <pthread.h> void pthread_exit(void* retval);
该函数通过
retval
参数向回收者传递其退出信息。它执行完之后不会回到调用者,而且永远不会失败。 -
pthread_join
一个进程中的所有线程都可以
pthread_join
函数来回收其他线程,即等待其他线程结束,这类似于回收进程的wait
和waitpid
系统调用。#include <pthread.h> int pthread_join(pthread_t thread, void** retval);
thread
参数是目标线程的标识符,retval
参数这是目标线程返回的退出信息。该函数会一直阻塞,直到被回收的线程结束为止。 -
pthread_cancel
有时候我们希望异常终止一个线程,即取消线程,它通过如下函数实现的:
#include <pthread.h> int pthread_cancel(pthread_t thread);
thread
参数是目标线程的标识符。该函数成功时返回0,失败则返回错误码。不过,接受到取消请求的目标线程可以决定是否允许被取消以及如何取消:#include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type, int *oldtype);
第一个函数用于设置线程的取消状态(是否允许取消),第二个用于设置取消类型(如何取消),参数
old*
用于记录原来的状态。
线程属性
pthread_attr_t
结构体定义了一套完整的线程属性,如下所示:
#include <bit/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
}pthread_attr_t;
可见,各种线程属性全部包含在一个字符数组中。线程库定义了一系列函数来操作pthread_attr_t类型的变量,以方便获取和设置线程属性。
POSIX信号量
POSIX信号量函数的名字都以sem_
开头,并不像大多数线程函数那样以pthread_
开头,常用的POSIX信号量函数有以下几个:
#include <semaphore.h>
// 初始化一个未命名的信号量
// pshared指定信号量的类型。如果其值为0,表示这个信号量是当前进程的局部信号量,
// 否则该信号量就可以在多个进程之间共享
int sem_init(sem_t* sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destory(sem_t* sem);
// 原子操作的方式将信号量的值减1.如果信号量为0,则sem_wait将被阻塞,
// 直到这个信号量非0
int sem_wait(sem_t* sem);
// 非阻塞版本,当信号量为0时,返回-1并设置errno为EAGAIN
int sem_trywait(sem_t* sem);
// 以原子操作的方式将信号量加1
int sem_post(sem_t* sem);
互斥锁
互斥锁可以用于保护关键代码段,以确保其独占式的访问。
POSIX互斥锁的相关函数主要有如下5个:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutextattr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
这些函数的第一个参数mutex
指向要操作的目标互斥锁,互斥锁的类型是pthread_mutex_t
结构体。mutexattr
指定互斥锁的属性,将它设置为NULL,则表示使用默认属性。
互斥锁还可以使用另外一种方式来初始化:
pthread_mutex_t = mutex = PTHREAD__MUTEX_INITIALIZER;
互斥锁属性
pthread_mutexattr_t
结构体定义了一套完整的互斥锁属性。线程库提供了一系列函数来操作pthread_mutexatttr_t
类型的变量,以方便获取和设置互斥锁属性。
互斥锁两种常用的属性:pshared
和type
。pshared
指定是否允许跨进程共享互斥锁;type
指定互斥锁的类型:
- 普通锁(默认):下列情况会有问题:一个线程内对一个已经加锁的锁再次加锁;对一个已经被其他线程加锁的普通锁解锁;对一个已经被解锁的普通锁解锁;
- 检错锁:会检错普通锁存在问题的情况,返回对应的错误码:重复加锁返回
EDEADLK
,重读解锁返回EPERM
- 嵌套锁:允许多次加锁,但是如果其他线程要获得这个锁,需要执行相应次数解锁。重复解锁会返回
EPERM
; - 默认锁:映射为上面三种锁之一。
条件变量
如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。
#include <pthread.h>
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);
int pthread_cond_destory(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
除了pthread_cond_init
函数外,我们还可以使用如下方式来初始化一个条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_broadcast
函数以广播的方式唤醒所有等待目标条件变量的线程。pthread_cond_signal
函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。
pthread_cond_wait
用于等待目标条件变量。mutex
参数是用于保护条件变量的互斥锁,以确保pthread_cond_wait
操作的原子性。在调用pthread_cond_wait
前,必须确保互斥锁mutex
已经加锁,否则将导致不可预期的结果。pthread_cond_wait
函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex
解锁。当pthread_cond_wait
函数成功返回时,互斥锁mutex
将再次被锁上。
多线程环境
可重入函数
线程安全的函数,如果一个函数能被多个线程同时调用且不发生竞态条件。
线程和进程
考虑这样一种情况:如果一个多线程程序的某个线程调用了fork函数,那么新创建的子进程会将父进程的执行线程完整的复制,并且子进程将自动继承父进程中互斥锁的状态。也就是说,父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就会引起一个问题:子进程可能不清楚从父进程继承而来的互斥锁的具体状态,可能在父进程已经被加锁了。这种情况下,子进程如果再加锁就会导致死锁。
pthread
提供一个专门的函数pthread_atfork
,以确保fork
调用后父进程和子进程都拥有一个清楚的锁状态。该函数定义如下:
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
该函数建立3个fork
句柄来帮助我们理清互斥锁的状态。
prepare
:fork
调用创建出子进程之前被执行;parent
:fork
调用创建出子进程之后,返回之后,在父进程中被执行;child
:fork
返回之前,在子进程中被执行。
线程和信号
每个线程可以独立地设置信号掩码,在多线程环境下pthread
版本的信号掩码函数:
#include <pthread.h>
#include <signal.h>
int pthread_sigmask(int how, const sigset_t* newmask, sigset_t* oldmask);
信号处理线程,专门的线程来处理所有的信号,实现步骤如下:
-
在主线程创建出其他子线程之前就调用
pthread_sigmask
来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。 -
在某个线程中调用如下函数来等待信号并处理之:
#include <signal.h> int sigwait(const sigset_t* set,int* sig)
set
参数指定需要等待信号的集合,sig
参数指向函数接收到的信号值,该函数会接收集合里的所有信号。