线程
基本概念
线程 VS 进程 多进程存在的一些问题:
- 进程间信息难以共享。由于除去只读代码段,父子进程并未共享内存,因此必须采用一些进程间通信方式,再进程间进行信息交换
- 调用fork()创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性 线程能够解决上述两个问题: 1.线程间能够方便,快速的共享信息只需将数据复制到共享(全局或堆)变量中即可。(注意多线程同时修改同个共享变量的问题) 2.线程创建比进程创建块10倍甚至更多。(linux中,通过clone()来实现线程的)
Pthreads API
pthreads数据类型: pthread_t: 线程ID
pthread_mutex_t: 互斥对象
pthread_mutexattr_t: 互斥对象属性
pthread_cond_t: 条件变量
pthread_condattr_t: 条件变量的属性
pthread_key_t: 线程特有数据的键
pthread_once_t: 一次性初始化控制上下文
pthread_attr_t: 线程的属性对象
pthreads函数返回值: Pthreads API:成功返回0,返回正值表示失败
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void(*start)(void*), void* arg);
//返回0代表成功,>0的值代表失败
新线程通过调用带有参数arg的函数start(即start(arg))而开始执行。调用pthread_create()的线程会继续执行该调用之后的语句。
将arg声明为void*类型,意味着可以将指向任意对象的指针传递给start()函数,如果需要向start传递多个参数,可以将arg指向一个结构体 参数thread指向pthread_t类型的缓冲区,在pthread_create返回之前,会在此保存一个该线程的唯一标识。后续的Pthreads函数将使用该标识来引用此线程
终止线程
可以如下方式终止线程的运行:
- 线程start函数执行return语句并返回指定值
- 线程调用pthread_exit()
- 调用pthread_cancel()取消线程
- 任意线程调用了exit(),或者主线程执行了return语句 pthread_exit()函数将终止调用线程,且其返回值可以由另一线程通过pthread_join()获得
#include <pthread.h>
void pthread_exit(void *retval);
调用pthread_cancel()相当于在线程的start函数中执行return
线程ID
一个线程可以通过pthread_self()来获取自己的线程
POSIX线程ID与linux专由的系统调用gettid()所返回的ID并不相同,POSIX线程ID由线程库实现来负责分配和维护,gettid()返回的线程ID是一个由内核分配的数字。
joining已终止的线程
pthread_join()等待由thread标识的终止线程(如果线程已经终止,pthread_join会立即返回)
int pthread_join(pthread_t thread, void**retval);
若retval非空,将会保存线程终止时返回值的拷贝
若线程并未分离(detached),则必须使用pthread_join()来进行连接。若线程未Join,那么线程终止时会产生僵尸线程。除了浪费系统资源外,僵尸线程积累过多,应用将再也无法创建新的线程
pthread_join() VS. 进程waitpid():
1.线程间的关系时对等的,即进程中的任意线程均可以调用pthread_join()与该进程的任何其他线程连接。这与进程间的层次关系不同,父进程如果使用fork()创建了子进程,那么它也是唯一能够对子进程调用wait()的进程
- pthread_join()无法连接任意线程,也不能以非阻塞方式进行连接(使用条件变量可以实现类似的功能)
#include <stdio.h>
#include <pthread.h>
static void* threadFunc(void*args){
char *s = (char*)args;
printf("str: %s \n", s);
return (void*)strlen(s);
}
int main(int argc, char*argv[]){
pthread_t t1;
void*res;
int s;
s = pthread_create(&t1, NULL, threadFunc, "Hello world.\n");
if(s!=0){
printf("create pthread error.");
exit(-1);
}
printf("Message from main()\n");
s = pthread_join(t1, &res);
if(s!=0){
printf("join failed.");
exit(-1);
}
printf("Thread returnd %\d\n", (long)res);
exit(0);
}
线程的分离
默认情况下,线程时可连接的,也就是说,当线程退出时,其他线程可以通过调用pthread_join()获取其返回状态。有时候,并不关心线程的返回状态,只是希望系统在线程终止时能自动清理并移除之
使用pthread_detach(),将线程标记为处于分离状态
int pthread_detach(pthread_t thread);
线程同步
互斥量:保护对共享变量的访问 多线程共享全局变量可能造成的问题
#include <stdio.h>
#include <pthread.h>
static int glob=0;
static void* trhreadFunc(void* args){
int loops = *((int*)args);
int loc,j;
for(j=0;j<loops;j++){
loc = glob;
loc++;
glob = loc;
}
return NULL;
}
int main(int argc, char*argv[]){
pthread_t t1, t2;
int loops, s;
if(argc<=1){
printf("need argc. \n");
return -1;
}
loops = atoi(argv[1]);
s = pthread_create(&t1,NULL, trhreadFunc, &loops);
if(s!=0){
printf("pthread create errror. \n");
return -1;
}
s = pthread_create(&t2, NULL, trhreadFunc, &loops);
if(s!=0){
printf("pthread create error. \n");
return -1;
}
s = pthread_join(t1,NULL);
if(s!=0){
printf("pthread1 join error. \n");
return -1;
}
s = pthread_join(t2,NULL);
if(s!=0){
printf("pthread1 join error. \n");
return -1;
}
printf("glob=%d \n", glob);
return 0;
}
避免线程更新共享变量时出现问题,使用互斥量来确保一次只有一个线程可以访问某项共享资源。
互斥量
互斥量可以像静态变量那样分配,也可以在运行时动态创建。
互斥量时属于pthread_mutex_t的变量,使用前必须对其初始化
1. 静态分配互斥量
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
互斥量的加锁和解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
调用pthread_mutex_lock时必须指定互斥量,如果互斥量当前处于未锁定状态,该调用将锁定互斥量并立即返回。如果其他线程已经锁定了该互斥量,那么pthread_mutex_lock()调用会一直阻塞,直到该互斥量被解锁,到那时,调用将锁定互斥量并返回
//使用互斥量保护对全局变量的访问
#include <stdio.h>
#include <pthread.h>
static int glob;
static pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
void* threadFunc(void* args){
int i,loop;
int loc;
int s;
loop = *((int*)args);
for(i=0;i<loop;i++){
s = pthread_mutex_lock(&mtx);
if(s!=0){
printf("get lock error. \n");
return NULL;
}
loc = glob;
loc = loc+1;
glob=loc;
s = pthread_mutex_unlock(&mtx);
if(s!=0){
printf("unlock error. \n");
return NULL;
}
}
return NULL;
}
int main(int argc, char*argv[]){
int s;
pthread_t t1, t2;
int loop;
if(argc<=1){
printf("need arg. \n");
return -1;
}
loop = atoi(argv[1]);
s = pthread_create(&t1, NULL, threadFunc, &loop);
if(s!=0){
printf("pthread create error. \n");
return -1;
}
s = pthread_create(&t2, NULL, threadFunc, &loop);
if(s!=0){
printf("pthread create error. \n");
return -1;
}
s = pthread_join(t1,NULL);
if(s!=0){
printf("pthread create error. \n");
return -1;
}
s = pthread_join(t2, NULL);
if(s!=0){
printf("pthread create error. \n");
return -1;
}
printf("glob: %d \n", glob);
return 0;
}
互斥量的死锁
当一个线程需要访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁
死锁例子如下:每个线程都成功锁上一个互斥量,接着试图对已为另一线程锁定的互斥量加锁,两个线程将无线等待下去
线程A 线程2
1 pthread_mutex_lock(mutex1); pthread_mutex_lock(mutex2);
2 pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex1);
避免此类死锁问题:定义互斥量的层级关系。当多个线程对一组互斥量操作时,总是应该以相同顺序对改组互斥量进行锁定。
还有一种方案,使用pthread_mutex_trylock(),调用失败(返回EBUSY),该线程将释放所有互斥量,经过一段时间间隔,从头再试【相比于按层级关系来规避死锁,这种效率更低一些,因为可能需要经理多次循环】
动态初始化互斥量
静态初始值PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量进行初始化:经由静态分配且携带默认属性。其他情况下,调用pthread_mutex_init()对互斥量进行动态初始化
int pthread_mutex_init(pthread_mutex* mutex, const pthread_mutex_attr_t *attr);
当不再使用时,使用pthread_mutex_destroy()销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
条件变量
互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(堵塞于)这一通知
生产者消费者,使用互斥量会存在的问题:
消费者代码会不停循环检查共享变量avail的状态,故而造成CPU资源的浪费
static pthread_mutex mtx = PTHREAD_MUTEX_INITIALIZER;
static int avail=0;
//生产者
s=pthread_mutex_lock(&mtx);
if(s!=0){
printf("get lock error.\n");
return;
}
avail++;
s=pthread_mutex_unlock(&mtx);
if(s!=0){
//error handle
return;
}
//消费者
for(;;){
s=pthread_mutex_lock(&mtx);
if(s!=0){
//error handle
}
while(avail>0){
avail--;
}
s=pthread_mutex_unlock(&mtx);
if(s!=0){
//errro
}
}
解决: 采用条件变量。允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作。(例如,出现一些“情况”后,等待着必须立即做出响应)
条件变量总是结合互斥量使用,条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥。 条件变量数据类型:
pthread_cont_t
静态分配的条件变量:
pthread_cond_t cond = PTHREAD_COND_INTTIALIZER;
通知和等待条件变量
条件变量的主要操作时发送信号(signal)和等待(wait).发送信号操作即通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变。等待操作是指再收到一个通知前一直处于阻塞状态
pthread_cond_signal()和pthread_cond_broadcast():针对由参数cond所指定的条件变量而发送信号。pthread_cond_wait()函数将阻塞一线程,直到收到条件变量cond的通知
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_signal()和pthread_cond_broadcast()的差别在于,二者对阻塞与pthread_cond_wait()的多个线程处理方式不同。pthread_cond_signal()只保证唤醒至少一条遭到阻塞的线程,ppthread_cond_broadcast()会唤醒所有遭阻塞的线程
使用函数pthread_cond_broadcast()总能产生正确的结果(因为所有线程应都能处理多余和虚假的唤醒动作),但pthread_cond_signal()更高效 考虑如下情况:
- 同时唤醒所有等待线程
- 某一线程首先获得调度。此线程检查了共享变量的状态(再相关互斥量的保护下),发现还有任务需要完成。该线程执行了所需工作,并改变共享变量状态,以表明任务完成,最后释放对相关互斥量的锁定
- 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,剩余的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用pthread_cond_wait())
pthread_cond_broadcast()适合处理的情况:处于等待状态的所有线程执行的任务不同
条件变量并不保存状态信息,只是传递应用程序状态信息的一种机制。发送信号时,若无任何线程在等待该条件变量,这个信号就会不了了之。线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态
pthread_cond_timedwait()和pthread_cond_wait(),区别在于前者由参数abstime来指定一个线程等待条件变量通知的时间上限
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
生产者消费者问题中使用条件变量
static pthread_cond_t *cond = PTHREAD_COND_INTTIALIZER;
static pthread_mutex_t *mtx = PTHREAD_MUTEX_INITIALIZER;
static int avail=0;
//生产者
s=pthread_mutex_lock(&mtx);
if(s!=0){
//errro
}
avail++;
s = pthread_mutex_unlock(&mtx);
if(s!=0){
//err
}
s=pthread_cond_signal(&cond); //wakeup sleeping consumer
if(s!=0){
//error
}
pthread_cond_wait()函数详解:前文已经指出,条件变量总是要与一个互斥量相关。将这些对象通过函数参数传递给pthread_cond_wait(),后者执行如下操作步骤:
- 解锁互斥变量mutex
- 阻塞调用线程,直至另一线程就条件变量cond发出信号
- 重新锁定mutex
s=pthread_mutex_lock(&mtx);
if(s!=0){
//error
}
while(/*check tnat shared variable is not in state we wante*/)
pthread_cond_wait(&cond, &mtx);
//当共享变量状态变为我们期望的状态时
......
s=pthread_mutex_lock(&mtx);
if(s!=0){
//error
}
以上代码,两处对共享变量的访问都必须置于互斥量的保护下:
- 线程在准备检查共享互斥量状态时锁定互斥量
- 检查共享变量状态
- 如果共享变量未处于预想状态,线程应在等待条件变量并进入休眠时解锁互斥量(以便其他线程能访问该共享变量)
- 当线程因为条件变量的通知而再度被唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量 pthread_cond_wait()会自动执行步骤3,4中的解锁和加锁动作
//消费者线程
for(;;){
s=pthread_mutex_lock(&mtx);
if(s!=0){
//error
}
while(avail==0){
//必须由while循环而不是if语句来控制对pthread_cond_wait()的调用,因为,
//当代码从pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该
//立即重新检查判断条件,在条件不满足的情况下继续休眠等待
s=pthread_cont_wait(&cond, &mtx);
if(s!=0){
//error
}
}
while(avail>0){
avail--;
}
s=pthread_mutex_unlock(&mtx);
if(s!=0){
//error
}
}
连接任意已终止线程
为每个命令行参数创建一个线程,每个线程休眠一段时间后随即退出,休眠时间由相应命令行参数所指定的秒数决定。
程序维护了一组全局变量,记录所有已创还能线程的信息。对每个线程,全局数组中都含有一组元素记录其线程ID以及当前状态(字段state)
状态字段可设置为以下值:
TS_ALIVE: 线程是活动的
TS_TERMINATED: 线程已终止但未连接
TS_JOINED: 线程终止且已被连接
//gcc thread_multijoin.c -lpthread -o thread_multijoin
//./thread_multijoin 1 1 2 3 3
//thread_multijoin.c
#include <stdio.h>
#include <pthread.h>
static pthread_mutex_t threadMutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t threadDied = PTHREAD_COND_INITIALIZER;
static int totThreads=0;
static int numLive=0;
static int numUnjoined=0;
enum tstate{
TS_ALIVE,
TS_TERMINATED,
TS_JOINED
};
static struct{
pthread_t tid;
enum tstate state;
int sleepTime;
}*thread;
static void* threadFunc(void*arg){
int idx =(int)arg;
int s;
sleep(thread[idx].sleepTime);
printf("Thread %d terminating\n", idx);
s=pthread_mutex_lock(&threadMutex);
if(s!=0){
printf("get lock error.\n");
return NULL;
}
numUnjoined++;
thread[idx].state = TS_TERMINATED;
s=pthread_mutex_unlock(&threadMutex);
if(s!=0){
printf("error.\n");
return NULL;
}
s = pthread_cond_signal(&threadDied);
if(s!=0){
printf("error.\n");
return NULL;
}
return NULL;
}
int main(int argc, char*argv[]){
int s, idx;
if(argc<2 || strcmp(argv[1], "--help")==0){
printf("need arg..\n");
return 0;
}
thread = calloc(argc-1, sizeof(*thread));
if(thread==NULL){
printf("calloc error.\n");
return 0;
}
//create threads
for(idx=0;idx<argc-1;idx++){
thread[idx].sleepTime = atoi(argv[idx+1]);
thread[idx].state = TS_ALIVE;
s = pthread_create(&thread[idx].tid, NULL, threadFunc, (void*)idx);
if(s!=0){
printf("error.\n");
return NULL;
}
}
totThreads = argc-1;
numLive = totThreads;
//join with terimed threads
while(numLive>0){
s=pthread_mutex_lock(&threadMutex);
if(s!=0){
printf("error.\n");
return NULL;
}
while(numUnjoined==0){
s = pthread_cond_wait(&threadDied, &threadMutex);
if(s!=0){
printf("error.\n");
return NULL;
}
}
for(idx=0; idx<totThreads; idx++){
if(thread[idx].state==TS_TERMINATED){
s= pthread_join(thread[idx].tid, NULL);
if(s!=0){
printf("error.\n");
return NULL;
}
thread[idx].state = TS_JOINED;
numLive--;
numUnjoined--;
printf("Reped thread %d (numLive=%d) \n", idx, numLive);
}
}
s = pthread_mutex_unlock(&threadMutex);
if(s!=0){
printf("error.\n");
return NULL;
}
}
return 0;
}
动态分配的条件变量
函数pthread_cond_init()对条件变量进行动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
当不再需要一个经由自动或动态发呢配的条件变量时,应该调用pthread_cond_destroy()函数销毁
int pthread_cond_destroy(pthread_cond_t *cond);