linux/unix系统编程----线程

97 阅读14分钟

线程

基本概念

线程 VS 进程 多进程存在的一些问题:

  1. 进程间信息难以共享。由于除去只读代码段,父子进程并未共享内存,因此必须采用一些进程间通信方式,再进程间进行信息交换
  2. 调用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函数将使用该标识来引用此线程

终止线程

可以如下方式终止线程的运行:

  1. 线程start函数执行return语句并返回指定值
  2. 线程调用pthread_exit()
  3. 调用pthread_cancel()取消线程
  4. 任意线程调用了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()的进程

  1. 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()更高效 考虑如下情况:

  1. 同时唤醒所有等待线程
  2. 某一线程首先获得调度。此线程检查了共享变量的状态(再相关互斥量的保护下),发现还有任务需要完成。该线程执行了所需工作,并改变共享变量状态,以表明任务完成,最后释放对相关互斥量的锁定
  3. 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,剩余的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用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(),后者执行如下操作步骤:

  1. 解锁互斥变量mutex
  2. 阻塞调用线程,直至另一线程就条件变量cond发出信号
  3. 重新锁定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 
}

以上代码,两处对共享变量的访问都必须置于互斥量的保护下:

  1. 线程在准备检查共享互斥量状态时锁定互斥量
  2. 检查共享变量状态
  3. 如果共享变量未处于预想状态,线程应在等待条件变量并进入休眠时解锁互斥量(以便其他线程能访问该共享变量)
  4. 当线程因为条件变量的通知而再度被唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量 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);