一、线程的概念

   进程在各个独立的地址空间中运行,进程之间共享数据需要mmap或者进程间通信机制,本节我们学习如何在一个进程的地址空间中执行多个线程。

   有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了场,如实现一个图形界面的下载软件,需要和用户交互,等待和处理用户的鼠标键盘事件,另一方面又需要同时下载多个文件,等待和处理从多个网络主机发来的数据,这些任务都需要一个“ 等待-处理”的循环,可以用多线程实现,一个线程专门 负责与用户交互,另外几个线程每个线程负责和一个网络主机通信。  

   main 函数和信号处理函数是同一个进程地址空间中的多个控制流程,多线程也是如此,但是比信号处理函数更加灵活,信号处理函数的控制流程只是在信号递达时产生,在处理完信号之后就结束,而多线程的控制流程可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。由于同一进程的多个线程共享同一地址空间,因Text Segment(代码段)Data Segment(数据段)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:  

  1. 文件描述符表  

   2. 每种信号的处理方式(SIG_IGN 、SIG_DFL或者自定义的信号处理函数) 

   3. 当前工作目录 

   4. 用户id 和组id

  但是有些资源是每个线程各自一份的: 

   1.线程id(由库函数<pthread.h>自己生成 pthread_t( unsigned long long))

   2.上下文,包括各种寄存器的值。程序计数器和栈指针

   3.栈空间

   4.errno变量

   5.信号屏蔽字

   6.调度优先级

  我们将要学习的线程库函数是有POSIX标准定义的,称为POSIXthread 或者pthread。 在Linux上 线程函数位于libpthread共享库中,因此编译时要加上 -lpthread 选项。

二、线程控制 

 A.创建线程

wKiom1cTLR3yxTdyAAAeC9uHV9M855.png

   返回值:成功返回0, 失败返回错误号。以前学过的系统函数都是成功返回0, 失败返回-1,错误号保存在全局变量errno中,pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno, 但这是为了兼容其它函数接口提供的,pthread 库本身并不使用它,通过返回值返回错误码更加清晰。  

   在一个线程中调pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决 定。start_routine函数接收一个参数,是通过pthread_createarg参数传递给它的,该参数的类型为

void *, 这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *, 这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程 可以调用pthread_join()得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态

   pthread_create成功返回后,新创建的线程的id 被填写到thread 参数所指向的内存单元。我们知进程id 的类型是pid_t,每个进程的id 在整个系统中是唯一的,调getpid(2) 可以获得当前进程的id,是一个正整数值。线程id 的类型是thread_t, 它只在当前进程中保证是唯一的,在不同的系统中thread_t 这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数printf打印,调用pthread_self(3) 可以获得当前线程的id 。  

    attr参数表示线程属性,再次不深入讨论线程属性,所有代码例子都传NULLattr参数,表示线程属性取缺省值。  

  如下是实例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* pthread_test(void* arg)
{
	int i=0;
	while(i++<10)
	{
		printf("this is thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());
		sleep(1);
	}
	return NULL;
}
	pthread_t ptr;

int main()
{
	int pthread_ret=pthread_create(&ptr,NULL,pthread_test,NULL);
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	int i=0;
	while(i++<10)
	{	
		printf("this is man thread  pid:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());
		sleep(1);
	}
	return 0;
}

运行结果:

wKiom1cTNfuB4N3bAAA8H7GMXnA863.png

   可知在Linux上,thread_t 类型是一个地址值, 属于同一进程的多个线程调getpid(2) 可以得到相同的进程号,调用pthread_self(3) 得到的线程号不相同由于pthread_create错误码不保存在errno中,因此不能直perror(3) 打印错误信息,可以先strerror(3) 把错误码转换成错误信息再打印。

   如果任意一个线程调用了exit 或_exit, 则整个进程的所有线程都终止,由于从main函数return也相当于调用exit, 为了防止新创建的线程还没有得到执行就终止,我们在main函数return 之前延 时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下面我们会看到更好的办法。  

   思考题:主线程在一个全局变量ntid 中保存了新创建的线程的id,如果新创建的线程不调用pthread_self 而是直接打印这个ntid, 能不能达到同样的效果?  

    解答:通过测试得到 线程创建成功时 新建线程的tid确实被保存在thread 地址 所指向的空间(图 1)。但有时我们为了快捷可能用同一个全局变量ntid 创建多个子进程 这时ntid肯定为最后创建的ntid值(图 2),因此只有pthread_self可以达到目的。(自己拙见) 

 图 1:

wKioL1cTPPWxcP1JAAA885y9mxY649.png

 图 2:

wKiom1cTPFfSKZRqAAA_eneo7Jo762.png

  B. 终止线程  

   如果需要只终止某个线程而不终止整个进程,可以有三种方法:   

   1. 从线程函数return 。这种方法对主线程不适和,从main函数return 相当于调用exit 。  

   2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。 

   3. 线程可以调pthread_exit终止。  

pthread_cancel终止一个线程分同步和异步两种情况,比较复杂,自此不介绍。

  下面介绍pthread_exit的和pthread_join的用法。

wKioL1cTRF3y5omqAAAZb8UZlAI862.png 

   retvalvoid *类型,和线程函数返回值的用法一样,其它线程可以调pthread_join获得这个指针。 

   需要注意,pthread_exit 或者return 返回的指针所指向的内存单元必须是全局的或者是malloc分 配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了


  c. 线程等待wKioL1cUmv6CfGVVAAAawHgJbaY555.png


  返回值:成功返回0, 失败返回错误号  

  调用该函数的线程将挂起等待,直到id 为thread 的线程终止thread 线程以不同的方法终止,通pthread_join得到的终止状态是不同的,总结如下: 

  1.如果thread 线程通过return 返回,value_ptr所指向的单元存放的是thread线程函数的返回值。  

  2.如果thread 线程被别的线程调pthread_cancel异常终掉,value_ptr所指向的单元存放的是常数PTHREAD_CANCELED 。 

  3.如果thread 线程是调用pthread_exit 终止的,value_ptr所指向的单元存放的是传给pthread_exit 的参数如果对thread 线程的终止状态不感兴趣,可以传NULL给value_ptr参数

 示例代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* pthread_test(void* arg)
{
	int i=0;
		printf("this is first thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());
		sleep(1);
	return (void*) 1;
}
void* pthread_test1(void* arg)
{
	int i=0;
		printf("this is secend thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());
	pthread_exit((void*)2);
}
void* pthread_test2(void* arg)
{
	int i=0;
		printf("this is third thread! pid:%d,tid: %u\n",(int)getpid(),(unsigned long)pthread_self());
	sleep(3);
	return NULL;
}

int main()
{
	pthread_t ptr1;
	pthread_t ptr2;
	pthread_t ptr3;
	void *tret;
	int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,NULL);
	pthread_join(ptr1,&tret);
	printf("thread return!,thread id:%u,return code:%d\n",(unsigned long)ptr1,(int)tret);
	int pthread_ret1=pthread_create(&ptr2,NULL,pthread_test1,NULL);
	pthread_join(ptr2,&tret);
	printf("thread exit!,thread id:%u,return code:%d\n",(unsigned long)ptr2,(int)tret);
	int pthread_ret2=pthread_create(&ptr3,NULL,pthread_test2,NULL);
	sleep(3);
	pthread_cancel(ptr3);
	pthread_join(ptr3,&tret);
	printf("thread be canceled!,thread id:%u,return code:%d\n",(unsigned long)ptr3,(int)tret);
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	
		printf("this is man thread  pid:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());
		sleep(1);
	return 0;
}

  运行结果:

wKiom1cUnG6y0kU_AAAjL-mzjsE348.png

  可见在Linuxpthread库中常数PTHREAD_CANCELED 的值是-1 。可以在头件pthread.h中找到它的定义 

般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach 状态,这样的线程一旦终就立刻回收它占有的所有资源,不保留终止状态。不能对一个已经处于detach 状态的线程调pthread_join,这样的调用将返回EINVAL。 对一个尚未detach 的线程调pthread_joinpthread_detach都可以把该线程置为detach 状态,也 就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调pthread_detach就不能再调pthread_join了。  

wKiom1cUngfjv3jTAAAYf2Xbr40291.png 

  返回值:成功返回0, 失败返回错误号。

三、线程分离

   在任何一个时间点上,线程是可结合的(joinable)或者是可分离的(detached)一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放 。 

    默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。 

    如果一个可结合线程结束运行但没有被join ,则它的状态类似于进程中的Zombie Process(僵尸进程),即还有部分资源没有被回收,所以创建线程者应该调pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。 

    由于调pthread_join 后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调pthread_join阻塞(因为还要继续处理之后到来的连接请求),这时可以在子线程中加入 

     pthread_detach(pthread_self()) 

或者父线程调 

     pthread_detach(thread_id) (非阻塞,可立即返回) 

    这将该子线程的状态设置为分离的(detached),如此一来,该线程运结束后会自动释放所有资源。

  代码实例(子线程分离结束 父线程得到返回值):

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* pthread_test(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n",arg);
	return NULL;
}

int main()
{
	pthread_t ptr1;
//	void *tret;
	int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,"chiled thread is run!");
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	sleep(1);//等待一秒让系统执行子线程
	if(0==pthread_join(ptr1,NULL))
	{
		printf("detach success!\n");
	}
	else
		printf("detach failed!\n");
	//printf("this is man thread  pid!!:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());
	return 0;
}

 运行结果:

wKiom1cUtKfB6WEhAAATqrw3_WU517.png

  代码实例(子线程分离未执行完,父线程已经非阻塞结束,使子线程也结束):

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* pthread_test(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n",arg);
	int i=0;
	for(;i<5;i++)
	{
		printf("run 5s! the %d s !\n",i);
		sleep(1);
	}
	return NULL;
}

int main()
{
	pthread_t ptr1;
//	void *tret;
	int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,"chiled thread is run!");
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	sleep(2);
	if(0==pthread_join(ptr1,NULL))//等待2秒后 子线程已经自己分离 因此会失败
	{
		printf("detach success!\n");
	}
	else
		printf("detach failed!\n");
	//printf("this is man thread  pid!!:%d,tid: %u!\n",(int )getpid(),(unsigned long long)pthread_self());
	return 0;
}

   运行结果:

wKioL1cUtvPQj3cFAAAVzNXmhww838.png

 在此只测试这两种情况!

四、线程同步与互斥

   A. mutex (互斥量) 

   多个线程同时访问共享数据时可能会冲突,这跟前讲信号时所说的可重入性是同样的问题。如两个线程都要把某个全局变量增加1, 这个操作在某平台需要三条指令完成:  

 1. 从内存读变量值到寄存器  

 2. 寄存器的值加1 

 3. 将寄存器的值写回内存  

假设两个线程在多处理器平台上同时执行这三条指令,则可能导致下图所示的结果,最后变量只加了一次而非两次。 

wKiom1cUuCuxhXDGAADYL8B-lNc032.png 思考:如果这两个线程在单处理器平台上执行,能够避免这样的问题吗? 

  解释(个人观点):全局变量只有一份,且能被多个子线程访问,那么他属于临界资源,并且+1操作在操作系统中局有原子性,那么在单系统中无论线程再多,对同一个全局变量的每次访问都是原子操作,那么就不会相互影响

  我们通过一个简单的程序观察这一现象。上图所描述的现象从理论上是存在这种可能的,但实际运行程序时很难观察到,为了使现象更容易观察到,我们把上述三条指令做的事情用更多条指令来做: 

   我们在“读取变量的值”和“把变量的新值保存回去”这两步操作之间插个printf调用,它会执 行write系统调用进内核(时间耗费大),为内核调度别的线程执行提供了一个很好的时机。我们在一个循环中重复上述操作几千次,就会观察到访问冲突的现象。

   代码如下(未处理线程互斥): 

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

 static int i=0;
void* pthread_test(void* arg)
{
	int tmp=0;
	int index=0;
	while(index++<5000)
	{
		tmp=i;
		printf("thread:%u run result:%d\n",(unsigned long)pthread_self(),tmp);
		i=tmp+1;
	}
	return NULL;
}
int main()
{
	pthread_t ptr1;
	pthread_t ptr2;
	int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,NULL);
	int pthread_ret1=pthread_create(&ptr2,NULL,pthread_test,NULL);
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	if(0==pthread_join(ptr1,NULL))
	{
		printf("detach success!\n");
	}
	else
		printf("detach failed!\n");
	return 0;
}

    运行结果(并非逻辑上的累加1000次):

wKioL1cUyD7y1IGHAAAgoeJZQPE005.png

  对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“ 读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成1个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并发做这个操作。 

   Mutexpthread_mutex_t 类型的变量表示,可以这样初始化和销毁: 

wKiom1cUyfrgCQInAAAl8e5Of04440.png   返回值:成功返回0, 失败返回错误号。  

   pthread_mutex_init 函数对Mutex做初始化,参数attr设定Mutex的属性,如果attrNULL则表示缺省属性,在此不详细介绍Mutex属性,感兴趣的读者可以参考[APUE2e]pthread_mutex_init 函 数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex 变量静态分配的(全局变量 或static 变量), 也可以宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于 pthread_mutex_init初始化并且attr 参数为NULLMutex加锁和解锁

操作可以用下列函数:

wKioL1cUy3fyXCxUAAAj0gDkc-s960.png   返回值:成功返回0, 失败返回错误号。  

 一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock 释放Mutex,当前线程被唤醒,才能获得该Mutex 并继续执行。  

   如果一个线程既想获得锁,又不想挂起等待,可以调pthread_mutex_trylock,如果Mutex经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

   代码如下:  

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
	
 pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;		
 static int i=0;
void* pthread_test(void* arg)
{
	int tmp=0;
	int index=0;
	pthread_mutex_lock(&mutex);
	while(index++<5000)
	{
		tmp=i;
		printf("thread:%u run result:%d\n",(unsigned long)pthread_self(),tmp);
		i=tmp+1;
	}
	pthread_mutex_unlock(&mutex);
	return NULL;
}
int main()
{
	pthread_t ptr1;
	pthread_t ptr2;
	int pthread_ret=pthread_create(&ptr1,NULL,pthread_test,NULL);
	int pthread_ret1=pthread_create(&ptr2,NULL,pthread_test,NULL);
	if(pthread_ret!=0)
	{
		printf("create pthread error!\n,info is:%s\n",strerror(pthread_ret));
		exit(pthread_ret);
	}
	pthread_join(ptr2,NULL);
	if(0==pthread_join(ptr1,NULL))
	{
		printf("detach success!\n");
	}
	else
		printf("detach failed!\n");
	return 0;
}

  运行结果:

wKiom1cUzkLiLbk-AAAlhh9q7w8802.png

    Mutex 的两个基本操作lock 和unlock 是如何实现的呢?假设Mutex变

量 的值为1表示互斥锁空闲,这时某个进程调lock 可以获得锁,Mutex的值为0表示互斥锁已经被 某个线程获得,其它线程再调lock 只能挂起等待。那么lock 和unlock 的伪代码如下:

  wKiom1cYY7nDqtf7AAB0cZ7m1jc945.png  

   unlock 操作中唤醒等待线程的步骤可以有不同的实现,可以只唤醒一个等待线程,也可以唤醒所有 等待该Mutex的线程,然后让被唤醒的这些线程去竞争获得这个Mutex,竞争失败的线程继续挂起等待。  

  大家已经看出问题了:对Mutex变量的读取、判断和修改不是原子操作。如果两个线程 同时调lock, 这时Mutex是1, 两个线程都判断mutex>0成,然后其中一个线程置mutex=0,而另一个线程并不知道这个情况,也置mutex=0, 于是两个线程都以为自己获得了锁。  

  为了实现互斥锁操作,很多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执时另一个处理器的交换指令只能等待总线周期。 现在我们把lock 和unlock 的伪代码改一下(以x86的xchg 指令为例):   

wKiom1cYZJiybhbaAACOr6pntwU382.png


   思考:xchgb 指令和条件判断之间有没有可能被切换出去?如果可能,会有什么问题? 

   解答:有可能被切出去,但是由于mutex的值得变化是与寄存器al(初值为0)交换而来,也就是说在执行xchgb命令前后,al的值和mutex的值中只可能有一个1,就算在执行过程中被切出去,这个1的归属也是确定的,因此判断语句进去的线程只可能有一个,所以说该逻辑正确,获得锁的线程只可能有一个

   unlock 中的释放锁操作同样只有一条指令实现,以保证它的原子性。  

也许还有人好奇,“ 挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一个项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤 醒的线程。  

  一般情况下,如果同一个线程先后两次调用lock,在第一次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然后锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此 就永远处于挂起等待状态了,这叫做死锁(Deadlock)(死锁 详解) 。另一种典型的死锁情形是这样:线程A获 得了锁1, 线程B获得了锁2, 这时线程A调用lock 试图获得锁2, 结果是需要挂起等待线程B释放 锁2, 这时线程B也调lock 试图获得锁1, 结果是需要挂起等待线程A释放锁1, 于是线程A和B都 永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会 变得复杂和难以判断。  

    写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有几个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex 变量的地址顺序)获得锁,则不会出现死 锁。假如一个程序中到锁1、锁2、锁3, 它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么 所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确 定一个先后顺序比较困难,则应该尽量使pthread_mutex_trylock调用代替pthread_mutex_lock 调用,以免死锁。(死锁详解:)

  B. Condition Variable (条件变量) 

   线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,等线程B在执行过程中使这个条件成立了,就唤醒线程A继续执。 在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程Condition Variablepthread_cond_t类型的变量表示,可以这样初始化和销毁:

wKiom1cYcMOxwR1yAAAlzGwTKdQ096.png  返回值:成功返回0, 失败返回错误号。  

   和Mutex的初始化和销毁类似,pthread_cond_init 函数初始化一个Condition Variable,attr参数 为NULL则表示缺省性,pthread_cond_destroy函数销毁一个Condition  Variable 。如果Condition Variable是静态分配的,也可以用宏定义PTHEAD_COND_INITIALIZER初始化,相当于

pthread_cond_init函数初始化并且attr参数为NULLCondition Variable的操作可以用下列函数:

wKiom1cYccGQQNCYAAAlwGD2Kto304.png   返回值:成功返回0, 失败返回错误号。 

   可见,一个Condition Variable 总是和一个Mutex 搭配使用的。一个线程可以调 用pthread_cond_wait 在一个Condition Variable上阻塞等待,这个函数做以下三步操作:   

  1. 释放Mutex

  2. 阻塞等待

  3. 当被唤醒时,重新获得Mutex并返回  

   pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的 时刻仍然没有别的线程来唤醒当前线程,就返  ETIMEDOUT。 一个线程可以调用pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调pthread_cond_broadcast 唤醒在这个Condition Variable上等待的所有线程。  

  下面的程序演示了一个生产者-消费者的例子,生产者生产一个结构体串在链表的表头上,消费者从表头取出结构体。  

wKiom1ccjzPBBrqKAAD2Th1cEd4778.pngwKioL1cckDaRefUHAAFADs3k35Y300.png


   运行结果(以上是一个消费者一个生产者):

wKiom1cckAXxw5AGAACqWvY-rX4291.png 

  1. 以上的例子中,生产者和消费者访问链表的顺序是LIFO的,请修改程序,把访问顺序改 成FIFO。 

  2. 将本例子改成多个消费者,多个生产者。

  代码如下: 

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<malloc.h>

 typedef struct list
 {
 	struct list*  _next;
	int _val;
 }list,*plist;

 int i=1;
plist  head=NULL;
plist  tail=NULL;
 pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
 pthread_cond_t need_product=PTHREAD_COND_INITIALIZER;

void init_node()
{	
	if(tail==NULL)
    {
		tail=(plist)malloc(sizeof(list));
		head=tail;
	}
	else	
	{
		tail->_next=(plist)malloc(sizeof(list));
		tail=tail->_next;
	}
	tail->_next=NULL;
	tail->_val=i++;
}

void* consumer1(void * val)
{
	while(i<10||head!=NULL)
	{
		pthread_mutex_lock(&mutex);
		if(NULL==head)
		{
			printf("没有产品!\n");
			pthread_cond_wait(&need_product,&mutex);
		}
		plist tmp=head;
		if(head==tail)
			tail=tail->_next;
		head=head->_next;
		pthread_mutex_unlock(&mutex);
		printf("consumer 1 use: %d\n",tmp->_val);
		free(tmp);
		sleep(1);
	}
	return NULL;
}

void* productor1(void* val)
{
	while(i<10)
	{
		sleep(1);
		pthread_mutex_lock(&mutex);
		init_node();
		printf("productor 1 run! produt:%d\n",tail->_val);
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(&need_product);
	}
	return NULL;
}
void* consumer2(void * val)
{
	while(i<10||head!=NULL)
	{
		pthread_mutex_lock(&mutex);
		if(NULL==head)
		{
			printf("没有产品!\n");
			pthread_cond_wait(&need_product,&mutex);
		}
		plist tmp=head;
		if(head==tail)
			tail=tail->_next;
		head=head->_next;
		pthread_mutex_unlock(&mutex);
		printf("consumer 2 use: %d\n",tmp->_val);
		free(tmp);
		sleep(1);
	}
	return NULL;
}

void* productor2(void* val)
{
	while(i<10)
	{
		sleep(1);
		pthread_mutex_lock(&mutex);
		init_node();
		printf("productor 2 run! produt:%d\n",tail->_val);
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(&need_product);
	}
	return NULL;
}

int main()
{
	pthread_t ptr1;
	pthread_t ptr2;
	pthread_t ptr3;
	pthread_t ptr4;
	pthread_create(&ptr1,NULL,consumer1,NULL);
	pthread_create(&ptr2,NULL,consumer2,NULL);
	pthread_create(&ptr3,NULL,productor1,NULL);
	pthread_create(&ptr4,NULL,productor2,NULL);
	pthread_join(ptr1,NULL);
	pthread_join(ptr2,NULL);
	pthread_join(ptr3,NULL);
	pthread_join(ptr4,NULL);
	return 0;
}

运行结果:

wKiom1cckUvDglBVAAAoG-7UL_A893.png 

  C. Semaphore (信号量)  

   Mutex变量是非0 即1 的, 可看作一种资源的可用数量, 初始化时Mutex是1, 表示有一个可用资源,加锁时获得该资源,将Mutex减到0, 表示不再有可用资源,解锁时释放该资源,将Mutex重新加到1, 表示又有了一个可用资源。  

   信号量(Semaphore)Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。即,如果信号量描述的资源数是1时,此时的信号量和互斥锁相同! 

   本节介绍的是POSIX semaphore 库函数,详见sem_overview(7), 这种信号量不仅可用于同一进程 的线程间同步,也可用于不同进程间的同步。  

wKioL1ccminSibKfAABK_C75SN8168.png

  semaphore 变量的类型为sem_t,sem_init() 初始化一个semaphore量,value 参数表示可用资源的数量,pshared 参数为0表示信号量用于同一进程的线程间同步(如何让他实现进程间的同步?),在此只介绍这种情况。在用完 semaphore 变量之后应该调sem_destroy() 释放与semaphore 相关的资源。

  调sem_wait()可以获得资源(P操作),使semaphore值减1, 如果调sem_wait() semaphore的值已 经是0, 则挂起等待如果不希望挂起等待,可以调sem_trywait()。调sem_post() 可以释放资源(V操作),使semaphore值加1, 同时唤醒挂起等待的线程。  

  上面生产者-消费者的例子是基于链表的,其空间可以动态分配,现在基于固定的环形队列重写这个程序(POSIX信号量): 

#include<stdio.h>
#include<semaphore.h>
#include<pthread.h>
#include<stdlib.h>
#define _SIZE_ 5

int a[_SIZE_]={0};
int i=0;
sem_t sem_pro,sem_con;

void* product(void* arg)
{
	int j=0;
	while(1)
	{
		printf("productor: %d\n",i);
		sem_wait(&sem_pro);
		a[j++]=i++;
		sem_post(&sem_con);
		j%=_SIZE_;
		sleep(1);
	}
	return NULL;
}

void* consume(void* arg)
{
	int j=0;
	while(1)
	{	
		sleep(2);
		sem_wait(&sem_con);
		printf("consumer: %d\n",a[j++]);
		sem_post(&sem_pro);
		j%=_SIZE_;
	}
	return NULL;
}

int main()
{
	pthread_t tid1,tid2;
	sem_init(&sem_pro,0,_SIZE_);
	sem_init(&sem_con,0,0);
	pthread_create(&tid1,NULL,product,NULL);
	pthread_create(&tid2,NULL,consume,NULL);
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	return 0;

}

  运行结果:

 wKioL1cfGRfzlWRbAAAgwH1NQzM966.png

  d. 读写锁 

    在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会较少。相比较改写,它们读的机会反而多的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。 

    读写锁就是用来处理这种情况的同步与互斥的。 

    读写锁实际是一种特殊自旋锁,它把对共享资源的访问者划分成读者写者读者只对共享资源进行读访问写者则需要对共享资源进行写操作。这种锁相对于自旋锁,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

    以下是读写锁的相关接口:

wKioL1cfGzOBKVWaAAAkikgvxUQ019.png

wKioL1cfGzOxKyuGAAAg_1fyhco480.png

wKiom1cfGmqwz9UEAAAhPiZEHww230.png

wKiom1cfGmrDj61qAAAaGPjBuo8064.png

   代码实例(一个写者,2个读者):

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>

int i=0;

pthread_rwlock_t lock;
void* writer(void * arg)
{
	while(1)
	{
		pthread_rwlock_wrlock(&lock);
		printf("writer run: %d\n",++i);
		pthread_rwlock_unlock(&lock);
		sleep(2);
	}
}

void* reader1(void* arg)
{
	while(1)
	{
		sleep(1);
		pthread_rwlock_rdlock(&lock);
		printf("reader1 run:%d\n",i);
		pthread_rwlock_unlock(&lock);
	}
}
void* reader2(void* arg)
{
	while(1)
	{
		sleep(1);
		pthread_rwlock_rdlock(&lock);
		printf("reader2 run:%d\n",i);
		pthread_rwlock_unlock(&lock);
	}
}

int main()
{
	pthread_t tid1,tid2,tid3;
	printf("start: i=%d\n",i);
	pthread_rwlock_init(&lock,NULL);
	pthread_create(&tid1,NULL,writer,NULL);
	pthread_create(&tid2,NULL,reader1,NULL);
	pthread_create(&tid3,NULL,reader2,NULL);
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	pthread_join(tid3,NULL);
	return 0;

}

   运行结果:
wKiom1cfJDXDd6XhAAAgNh0kem8603.png