概述
- 线程和进程本质上来说都属于一个内核调度单元,也就是说都可以作为一条单独的执行路径。但是多进程程序通常有一些限制,比如说创建一个进程开销很大并且进程间通信比较困难;而线程就是为了打破这些限制而诞生的。由于多个线程同属于一个进程,所以多个线程之间共享数据段,堆段,共享内存以及位于栈区的共享库。这就意味着在多个线程之间共享数据十分方便。另外创建一个线程的开销也很小。
- 除了全局内存以外,线程间还共享以下内容
- 进程ID
- 进程组ID和会话ID
- 控制终端
- 用户ID和组ID
- 打开的文件描述符
- 信号处理程序
- 当前工作目录,文件权限掩码
- 定时器
- 资源限制
- fcntl创建的记录锁
- 线程ID
- 信号掩码
- 本地变量
- errno
- 线程特有数据
数据类型 |
描述 |
pthread_t |
线程ID |
pthread_mutex_t |
互斥对象 |
pthread_mutexattr_t |
互斥属性对象 |
pthread_cond_t |
条件变量 |
pthread_condattr_t |
条件变量的属性对象 |
pthread_key_t |
线程特有属性的键值 |
pthread_once_t |
一次性初始化控制上下文 |
pthread_attr_t |
线程的属性对象 |
线程的创建和终止
- 在编译使用了pthread API的程序时需要指定libpthread库,即增加编译选项-lpthread
- 使用pthread_create函数创建一个新的线程,原型为
int pthread_create(pthread_t* thread,const pthread_attr* attr,void* (*start)(void*),void* arg)
,thread是放置线程ID的缓冲区;attr是创建线程使用的线程属性,一般设置为NULL;start是一个函数指针,指向该线程的执行函数;arg是传递给该线程的参数,可以在start函数中获取,一般是全局变量或者堆变量
- 可以使用如下方式终止线程的运行:1.使用return返回并指定返回值。2.调用pthread_exit。3.调用pthread_cancel。4.任意线程调用exit将终止同一个进程的所有其他线程。
- pthread_exit的函数原型为
void pthread_exit(void* retval)
,retval返回值可以由其他线程调用pthread_join获得;但是对于线程的返回值不应该放在线程私有栈中,因为线程结束后该线程栈被释放,从而数据就不正确了;
- 可以调用pthread_self查看自身的线程ID,原型为
pthread_t pthread_self(void)
;另外,在调用pthread_create创建新线程时,如果创建成功会返回对应的线程ID;对于两个线程ID的比较不能使用==而要调用pthread_equal,其原型为int pthread_equal(pthread_t t1,pthread_t t2)
- 使用pthread_join可以连接一个线程,即等待特定线程终止,获取其返回值。如果线程未被分离,则必须由另一线程调用此函数连接,否则会产生僵尸线程。另外,进程中的任意线程都可以连接其他线程。其函数原型为
int pthread_join(pthread_t thread,void** retval)
,如果retval非空值将会保存待连接线程的退出值(包括return和pthread_exit)。如果连接成功返回0,否则返回错误值(直接返回errno)
- 默认情况下,线程是可连接的,也就是说在线程退出时,其他线程可以通过pthread_join来获得退出值。但是如果不关心线程的退出值,那么可以让系统去负责终止线程的回收工作。在这种情况下,需要调用pthread_detach来使得线程处于分离状态。函数原型为
int pthread_detach(pthread_t thread)
,如果成功调用函数返回0,否则返回错误值。另外,如果一个线程已经处于分离状态,那么其他线程就不能再使用连接函数去回收该线程
线程同步
- 为避免线程修改共享变量时出现问题,必须使用互斥量来确保同一时刻只有一个线程能够访问共享变量。互斥量只有两种状态,已锁定和未锁定状态。一旦线程锁定一个互斥量,那么该线程成为该互斥量的所有者,也只有互斥量所有者才能为其解锁,并且只有当互斥量处于未锁定状态时才能被线程占有
- 互斥量可以有两种初始化方式,其一是通过静态分配,即
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
;其二是通过pthread_mutex_init方法动态初始化
- 在初始化后,互斥量处于未锁定状态。函数pthread_mutex_lock可以锁定一个互斥量,而函数pthread_mutex_unlock可以将互斥量解锁。如果所指定的mutex已经被锁定(被某个线程占有),那么调用pthread_mutex_lock的线程将被阻塞,直到该mutex解除锁定状态。并且如果有多个线程同时等待同一个互斥量,那么系统会随机选一个线程,而不是确定的。对于被自己锁定过的互斥量进行加锁要根据互斥量的不同属性(可重入性)来判别。对于一个未被锁定或者被其他线程锁定的互斥量调用unlock会出错。两个函数的原型如下
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
- 由于pthread_mutex_lock函数在某些时刻会阻塞调用线程,而我们又不想发生这样的情况。这时候我们就需要pthread_mutex_trylock和pthread_mutex_timedlock函数。如果指定的mutex已经被锁定,对其执行trylock会失败并立即返回EBUSY错误;而如果对其调用timedlock则可以指定一个阻塞时间,如果超过这个阻塞时间,那么就返回失败
- 为避免死锁的出现,应该遵循一个原则,当多个线程访问一组互斥量时,应当以相同的顺序进行锁定;或者可以避免使用lock而代替使用trylock
- 当互斥量是在共享内存区域中分配或者需要使用非默认的互斥量属性时需要使用动态分配的方式;并且当使用完毕以后,需要调用pthread_mutex_destroy函数将其销毁。如果互斥量所在的内存是动态分配的,在调用destroy函数之前,还需要先调用free函数将内存释放。两个函数的原型如下
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
- 对于pthread_mutexattr_t类型的设置如下代码所示,与互斥量的创建类似
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex,&attr);
pthread_mutex_destroy(&mutex);
- 条件变量总是结合互斥量一起使用,条件变量就共享变量的状态改变而发出通知,而互斥量则提供对共享变量的互斥访问。条件变量也分为静态和动态两种分配方式。静态分配方式为
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
- 对条件变量的操作方式主要有两类,分别是唤醒和等待。分别对应pthread_cond_signal()和pthread_cond_wait()两个函数。需要注意的是wait函数的工作方式,之前说过条件变量需要结合互斥量一起使用,所以wait函数的工作流程是先解锁mutex(以便能够让其他线程锁定该mutex,不至于造成死锁),然后阻塞工作线程直到另外的线程唤醒它,最后在锁定mutex。另外,pthread_cond_broadcast函数会唤醒所有在等待该条件变量的线程,函数原型如下
#include <pthread.h>
int pthread_cond_signal(pthread_cont_t* cond);
int pthread_cond_broadcast(pthread_cont_t* cond);
int pthread_cond_wait(pthread_cont_t* cond,pthread_mutex_t* mutex);
- 在测试条件变量的判断条件时,需要使用while循环而非if判断。理由是可能不止一个线程在等待该条件变量,也就是说条件变量的满足并非等同于程序所需要的判断条件满足,所以应该循环判断
- 可以通过pthread_cond_init对条件变量进行动态分配,与mutex的动态分配类似,也会有一个配套的destroy函数
线程特有数据
- 线程安全函数是指能够被多个线程同时安全调用的函数,这和可重入函数有点类似;但是可重入函数是线程安全函数的一个子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数(因为可重入函数不仅可以工作于不同线程之间,也可以工作在不同进程中间)
- 有时候多线程程序会有这样的需求,即不管创建了多少线程,一些初始化函数只调用一次,这对于一些库函数来说十分常见。这可以使用pthread_once函数来实现,once_control是一个指向pthread_once_t类型变量的指针,该参数用来确保只调用一次由init指针所指向的初始化函数。在使用之前,pthread_once_t变量应该这样初始化
pthread_once_t once = PTHREAD_ONCE_INIT
,另外该函数原型为int pthread_once(pthread_once_t* once_control,void (*init)(void))
- 线程特有数据可以使得函数为每一个调用线程维护一份变量的副本(互相独立)。首先需要调用
int pthread_key_create(pthread_key_t* key,void (*destructor)(void*))
来创建一个key来区分不同的线程特有数据,并且该初始化函数只需要调用一次,一般会使用pthread_once来完成。另外还可以传入一个解构函数指针destructor,用来释放与每个线程特定的数据资源;在创建完key后,需要创建每个线程独立的存储数据的内存块(通过使用malloc等函数),每个线程调用函数时只会申请一次内存;然后调用int pthread_setspecific(pthread_key_t key,const void* value)
将之前申请的内存空间与key相关联;在之后调用void* pthread_getspecific(pthread_key_t key)
时,会直接返回与key关联的内存块。如果是首次调用,即还没调用setspecific完成绑定,那么getspecific会返回NULL。示例代码如下(假设func是会被多个线程调用的函数)
#include <stdio.h>
#include <pthread.h>
#define BUFFSIZE 4096
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t key;
void finalize(void* buf)
{
free(buf);
}
void createKey(void)
{
int res = pthread_key_create(&key,finalize);
if(res != 0)
/*出错处理*/
}
void func()
{
int res;
char* buf;
res = pthread_once(&once,createKey);
if(res != 0)
/*出错处理*/
buf = (char*)pthread_getspecific(key);
if(buf == NULL)
{
buf = (char*)malloc(BUFFSIZE);
pthread_setspecific(key,buf);
}
/*其他处理*/
}
线程取消
- 一般线程只能主动退出或终结,但是可以通过
int pthread_cancel(pthread_t thread)
线程取消函数来终止一个指定线程。但是调用该取消函数会立即返回,这意味着指定的线程不会立即被取消,而线程何时取消以及会不会取消都要取决于该线程自身的线程取消状态和类型属性
- 线程可以调用
int pthread_setcancelstate(int state,int* oldstate)
和int pthread_setcanceltype(int type,int* oldtype)
来设置自身的取消状态和类型,oldstate和oldtype缓冲区可以获得原来的设定值。线这两种属性有以下的一些取值:线程取消状态PTHREAD_CANCEL_DISABLE,表示线程不可取消,如果线程收到了取消请求,那么会将该请求挂起直到线程取消状态改变;线程取消状态PTHREAD_CANCEL_ENABLE,表示线程可以取消,这是线程取消状态的默认值,但是即使处于这个状态,也并不意味着线程会被取消。还需要取决于线程类型值(type)。如果type是PTHREAD_CANCEL_ASYNCHRONOUS,表示可能会在任意时间点取消线程,并不确定,这被称之为异步取消;如果type是PTHREAD_CANCEL_DEFERED,取消请求会被挂起直到到达取消点(下面介绍取消点)
- 取消点实际上就是一个函数,如果线程收到了一个取消请求并且设置了取消状态且取消类型被置为延迟,那么该线程就会被终止。另外,如果该线程没有被分离,需要其他线程对其进行连接,pthread_join调用的第二个参数会被置为PTHREAD_CANCELED
- 并不是所有的函数都是取消点,如果线程的执行函数中没有取消点,那么就不会响应取消请求。这时候我们需要调用
void pthread_testcancel(void)
函数来创造一个取消点。如果调用线程有一个取消请求被挂起,那么调用该函数会使得线程立即终止
线程和信号
- 信号属于进程层面,如果某一进程收到一个信号且其默认动作为stop或者term,那么将停止整个进程和所有线程
- 进程中的所有线程共享为每个信号设置的信号处理程序
- 信号的发送既可以面向进程也可以面向线程单独发送。当面向一个进程发送信号时,进程会随机选择一个线程来接收该信号并处理。只有当信号的来源是以下三种时,才会面向线程发送信号:其一是源于线程上下文中对特定硬件指令的执行,即SIGBUS,SIGFPE,SIGILL,SIGSEGV;其二是跟管道相关的信号SIGPIPE;其三是通过pthread_kill或者pthread_sigqueue发出的信号
- 上面说过信号掩码是线程独有的,是针对每一个进程而言的;可以调用
int pthread_sigmask(int how,const sigset_t* set,sigset_t* oldset)
进行设置。另外,内核会为所有线程和进程各自维护信号挂起集合,函数sigpending会返回为整个进程和当前线程所挂起信号的并集
- 函数
int pthread_kill(pthread_t thread,int sig)
可以向指定线程发送信号而不是面向进程发送信号
- 在多线程程序中调用exec后,将执行程序替换并且只会留下调用线程,其他线程都会消失(包括所有的线程对象,比如互斥量,条件变量等);调用fork之后,子进程只会复制调用线程而抛弃其他线程,但是会留下线程对象(这会出现一些问题,比如一些互斥量没办法解锁)。所以在多线程程序中执行fork一般都会在exec之后