1. 线程基础
重点:
l 进程和线程的区别
l 进程和线程的关系
l POSIX线程库和编译多线程的选项
l 多线程编程
ü 如何创建一个线程,如何给工作函数传入参数
ü 如何结束一个线程,如何给工作函数传出返回值
1.1 回顾进程 – P48
OS有一个很重要的功能就是封装了对系统资源的管理,目的是提供一种机制更好地让多用户共享有限的资源 – 资源总是有限的。
Ps:共享的资源不仅是cpu,可以是计算机的一切资源,比如内存,IO设备等等
操作系统要支持共享资源的很重要的一个功能就是支持多任务,现代OS引入多进程概念,目的也是为了共享资源。支持了多任务就涉及到进程调度。那当我们在时间片分配,导致进程调度时,也就是上下文切换。
解释:上下文切换,上下文是什么意思呢,我们运行一个进程所需要的资源,我们把它称为上下文,那就有内存,快速缓存中的数据,pc指针等等。那上下文切换就要替换原先内容。
上下文切换时OS系统大体帮我们做了两件事如下:
1. 切换页目录(TLB)以使用新的地址空间和切换cache。解释:页目录是什么。我们前面学习过进程已经知道,每创建一个进程都有一个进程表项对应着4G的内存空间,那在内部是如何描述这个进程表项呢?我们可以进入内核查看一下定义,描述进程是用一个task_struct结构体。由于task_struct比较庞大,我们就不一一作分析了,有兴趣的可以课后分析查看一下,在其中有一个mm_struct结构的mm指针,它是用来指向我们的虚拟4G空间的首地址的。也就是真正的一个进程在内部是由一个task_struct结构体和一个虚拟空间组成的。
2. 切换内核栈和硬件上下文。
如果我们进程有很多,每次上下文切换都要做这些事,还是很耗资源的,所以很多操作系统中都引入了轻量级的进程的概念,也称之为线程。线程是在进程的基础上进一步对程序的运行进行细粒度的划分。
1.2线程的概述 (P49-P50)
1.2.1 多线程程序设计的优点可以如下四类:
参考http://wenku.baidu.com/view/810983ea998fcc22bcd10d2e.html
1. 经济实惠:为进程创建分配存储器和资源代价高昂。因为线程共享它们所属进程的资源,所以线程的创建和上下文转换更为划算。
l 线程的创建,很明显除了第一个线程是伴随进程的创建而出现,随后在同一个进程内的线程的创建,需要分配的资源就少了。
l 更快的调度速度:线程切换为何比进程切换快:线程共享进程的地址空间。刚才我们说过进程上下文切换要做两件事,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
l 创建和维护进程与创建和维护线程的开销孰大孰小难以测量(一般根据经验判断),但是通常创建和管理进程比创建和管理线程需要更多的时间。在Solaris 2中,创建线程的速度比创建进程快30倍,上下文转换速度快5倍。
2. 资源共享:缺省情况下,线程共享它们所属进程的存储器和资源。代码共享的优点在于它允许应用程序在同样的地址空间内拥有多个不同的活动线程。更方便地共享数据。对一些数据进行共享时不用涉及进程间通讯。具体可以共享哪些请参考ppt P52
3. 提高了响应速度:这个是从用户体验上来说的。多线程交互式应用程序可以允许程序在它的一部分被阻塞或正在执行一个冗长的操作时持续运行,从而提高了了对用户的响应速度。例如,一个多线程网页浏览器可以在一个线程下载图片时利用另外一个线程与用户交互。
4. 提高了多处理机体系结构的利用率:在多处理机体系结构中,多线程的优点就更加显著了。在这种系统中,多个线程可以在不同的处理器上并行运行。一个单线程进程只能够在一个CPU上运行,而不论有多少CPU可供使用。在多CPU机器中,多线程提高了并发性。在单处理机体系结构中,CPU通常快速的在每个线程之间移动,如此以至于用户感觉到这是在并行运行(这是个假象),但是实际上同时只有一个线程在运行。
1.2.2 Linux实现线程的机制
Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有为线程定义特别的数据结构和特别的调度算法,线程只是被视为一个与其他进程共享某些资源(比如地址空间)的进程。每个线程都拥有唯一的属于自己的task_struct。所以在内核里,它看起来就像是一个普通的进程。
举例:<<<<<< Shell下查看线程:
ps 命令,-L参数显示进程,并尽量显示其LWP(线程ID)和NLWP(线程的个数)。
# ps -eLf | grep ^syslog
PID PPID LWP NLWP
syslog 525 1 525 0 3 Apr21 ? 00:00:02 rsyslogd -c4
syslog 525 1 535 0 3 Apr21 ? 00:00:04 rsyslogd -c4
syslog 525 1 536 0 3 Apr21 ? 00:00:00 rsyslogd -c4
第二列为PID,第三列为PPID,第四列为LWP,第六列为NLWP
为确保显示
pstree 命令,查看进程和线程的树形结构关系
pstree -p | grep syslog
|-rsyslogd(525)-+-{rsyslogd}(535)
| `-{rsyslogd}(536)
说明一下线程线程组的概念
参考:http://blog.chinaunix.net/uid-24774106-id-3650136.html
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader; /* threadgroup leader */
上面三个变量是进程描述符的三个成员变量。pid字面意思是process id,其实叫thread id会更合适。tgid字面含义是thread group ID。对于存在多个线程的程序而言,每个线程都有自己的pid,没错pid,如同我们例子中的525/535/536,但是都有个共同的线程组ID(TGID):525。
好吧,我们再重新说一遍,对于普通进程而言,我们可以称之为只有一个LWP的线程组,pid是它自己的pid,tgid还是它自己,线程组里面只有他自己一个光杆司令,自然group_leader也是它自己。但是多线程的进程(线程组更恰当)则不然。开天辟地的main函数所在的进程会有自己的PID,也会有也TGID,group_leader,都是他自己。注意,它自己也是LWP。后面他使用ptherad_create创建了2个线程,或者LWP,这两个新创建的线程会有自己的PID,但是TGID会沿用创建自己的那个进程的TGID,group_leader也会尊创建自己的进程的进程描述符(task_struct)为自己的group_leader
我们传说的getpid函数,本质取得是进程描述符的TGID,而gettid系统调用,取得才是每个LWP各自的PID
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1.3 线程库的历史 (P51)
参考:Linux线程模型的比较:LinuxThreads和NPTL (有点老但比较经典)
http://www.ibm.com/developerworks/cn/linux/l-threading.html
wiki Native POSIX Thread Library
http://zh.wikipedia.org/wiki/Native_POSIX_Thread_Library
针对课件需要说明如下几点
l NPTL的发展历史:当 Linux最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。(有点类似前面描述过的vfork)。LinuxThreads项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合 POSIX的要求。要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL项目。NGPT在 2003年中期被放弃了,把这个领域完全留给了 NPTL。NPTL,或称为Native POSIX Thread Library,是Linux 线程的一个新实现,它克服了LinuxThreads 的缺点,同时也符合POSIX 的需求。与LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与LinuxThreads 一样,NPTL也实现了一对一的模型。NPTL最开始在redhat linux9里发布,从第3版开始NPTL是Red Hat Enterprise Linux的一部分,从Linux内核2.6开始它被纳入内核。目前它完全被结合入GNU C 库。
l 什么叫1对1,意思是说当你用pthread_create()创建一个线程后,在内核里就相应创建一个调度实体,也就是我们说过的task_struct。
l 性能上举个例子:在同样的硬件条件下,在32位机下,NPTL启动100000个线程只用了2秒,而相比不利用NPTL的则花费了15m~。
我们要知道,一个进程里可以有多个线程并发执行。我们在前面创建进程时呢是用fork的,那总不会创建线程也是fork吧,其实呢,我们线程库给我们提供了专门的线程处理函数,不是我们的标准库函数,属第三方库,所以在编译时要加入-lpthread.
1.4 线程与进程的异同点 (P52,P53)
线程就理解为一个执行者好了,它没有资源。进程概念上是资源的宿主。Linux上的理念是大家都是执行者,资源共享。
l 相同点
<1> 比如都具有ID,一组寄存器,状态,优先级以及所要遵循的调度策略。
<2> 每个进程都有一个进程控制块,线程也拥有一个线程控制块(在Linux内核,线程控制块与进程控制块用同一个结构体描述,即struct task_struct),这个控制块包含线程的一些属性信息,操作系统使用这些属性信息来描述线程。
<3> 线程和子进程的创建者可以在线程和子进程上实行某些控制,比如,创建者可以取消、挂起、继续和修改线程和子进程的优先级。
l 不同点
<1> 主要区别:每个进程都拥有自己的地址空间,但线程没有自己独立的地址空间,而是运行在一个进程里的所有线程共享该进程的整个虚拟地址空间。
<2> 线程的上下文切换时间开销比进程上下文切换时间开销要小的多
<3> 线程的创建开销远远小于进程的创建
<4> 子进程拥有父进程的地址空间和数据段的拷贝,因此当子进程修改它的变量和数据时,它不会影响父进程中的数据,但线程可以直接访问它进程中的数据段。
<5> 进程之间通讯必须使用进程间通讯机制,但线程可以与进程中的其他线程直接通讯
<6> 线程可以对同一进程中的其他线程实施大量控制,但进程只能对子进程实施控制
<7> 改变主线程的属性可能影响进程中其他的线程,但对父进程的修改不影响子进程
1.5 多线程编程
API 讲解P54-P57
可以与进程的函数对应起来讲
进程 | 线程 | 注释 |
pid | pthread_t | l 线程ID,进程ID在整个系统中是唯一的(非负整数),但线程ID不同,线程ID只在它所属的进程环境中有效 l pthread_t从可移植的角度来讲,其定义是一个结构,不要简单地将其看做一个整数,如果要比较,需要有专门的函数pthread_equal l 如何打印pthread_t:考虑到可移植性,这个数据结构在不同的OS上实现是不同的,所以没有一个简单的统一打印方式。(网上有一个就是直接打印结构的二进制buffer,但没有必要),如果确定在Linux上可以查看Linux的实现方式决定打印方式。<bits/ pthreadtypes.h> /* Thread identifiers. The structure of the attribute type is not exposed on purpose. */ typedef unsigned long int pthread_t; 所以就Linux来说,可以直接打印%lu |
getpid | pthread_self | 获取自身的线程ID |
fork | int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void * (* routine)(void *), void *arg ) | l 形参: ² thread:指向pthread_t类型的指针,该地址将存放线程创建成功之后的线程TID。 ² attr:用户设置线程的属性,一般都不需要特殊设置,所以可简单设置为NULL。 ² *(*start_routine)(void *):传递新线程所要执行的函数的地址。注意:我们可以看到,如果想启动一个线程,就必须让这个线程关联一个子函数。我们一般称此函数为线程函数, ² arg:新线程所有执行的函数的参数,标准线程创建接口只留了 一个参数传递。思考?如果想给线程函数传递多个参数,该怎么解决呢?A:如果需要传递的参数不止一个,那么需要把参数放到一个结构里 l 返回值由函数直接返回,不像其他POSIX函数设置errno。(这个教材上讲的有问题) 举例:<<<<samples\2.Threads\1.5-mthreads\thread.c 讲解重点: 1)线程的create涉及的入参 2)-lpthread 3)-D_REENTRANT:生成可重入代码 在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个单独的全局性区域来缓存输出数据。为解决这个问题,需要使用可重入的例程。可重入代码可以被多次调用而仍然工作正常。编写的多线程程序,通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须出现于程序中的任何#include语句之前。 _REENTRANT为我们做三件事情,并且做的非常优雅: (1)它会对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r。 (2)stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数。 (3)在error.h中定义的变量error现在将成为一个函数调用,它能够以一种安全的多线程方式来获取真正的errno的值。<<<<<<<<<<<<<<<<<<<< |
exit | int pthread_exit(void *value_ptr) | 在不终止整个进程的情况下,单个线程可以有三种方式停止其工作流并退出: l 线程从其工作函数中返回,返回值是线程的退出码 l 线程可以被同一进程中的其他线程取消 l 线程自己调用pthread_exit 注意:返回value_ptr时不能将该指针指向线程工作函数内的局部变量的地址,思考?局部变量存在在栈上,线程函数退出后,这个局部变量无效了,有可能在join时得到的会是无效值。所以如果要传一个结构,最好定义在堆上或者使用全局变量。 void *workfunc(void *arg) { int local = 0; pthread_exit(&local); // danger! } 举例:<<<<<<<<<<演示exit返回值的处理方式 samples\2.2-Process\2.Threads\ 1.5-mthreads\ thread_exit.c <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |
waitpid | int pthread_join(pthread_t thread, void **value_ptr) | 一般此函数用在主线程中,等待通过thread指定的线程终止,此函数调用成功,可以通过value_ptr获取终止线程的返回值。 注意:如果等待的线程没有终止,此函数将引起调用者阻塞。成功返回0,失败返回-1。 线程执行完后如果不join的话,线程的资源会一直得不到释放而导致内存泄漏!所以需要join或者detach,本课程不讲detach,有兴趣学生自己回去看。 |
abort | int pthread_cancel(pthread_t thead) | 线程通过调用pthread_cancel来请求取消同一个进程中的其他线程。pthread_cancel并不等待线程终止,仅提出请求。 |
举例:<<<<<<<<这个例子列举了线程退出的三种典型方式。
samples/2.2-Process/2.Threads/ 1.5-mthreads/ thread_term.c
l 自然退出 return
l Call pthread_exit()
l Call pthread_cancel() >>>>>>>>>>>>>
作业 <<<<<<<<<练习创建子线程
Main创建两个子线程(用伪代码给学生讲一下先)
子线程1循环3次,每次sleep一秒后打印一行十个A字符
子线程2循环3次,每次sleep一秒后打印一行十个B字符
多次执行程序,看看屏幕输出
是否发现A和B的行会交替出现。
或者让学生思考一下,
1) 如果我们不想A和B的行交替出现,而是打印三行A再三行B或者反之 三行B再三行A也可以
2) 再进一步,我们要求必须总是先打三行A再打三行B
这个作业和下面章节要讲的互斥的例子是相关的,只是铺垫一下。
例子在samples\2.2-Process\2.Threads\ 1.5-mthreads\AB.c >>>>>>>>>>>>>>>>>>>>>
2. 线程的同步和互斥
重点:
l 临界资源,互斥,同步的概念
l Linux上对互斥和同步的支持方法。
注:这一段我讲解的顺序和课件稍有差别,请同学们注意一下。
多线程之间通讯的方式比较简单,直接共享进程内资源+函数调用就可以了。关键是多线程之间的同步和互斥的操作。而进程较之相对复杂,首先会涉及到进程间通讯的问题。所以我们在讲解线程时先引入同步和互斥的概念。后面讲进程间通讯IPC时,也会再次回顾同步和互斥的概念。
2.1 基本概念P62-P65
l 临界资源:首先谈一下临界资源的概念:某些资源来说,其在同一时间只能被一段机器指令序列所占用。这些一次只能被一段指令序列所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程/线程所共享的一些变量和数据等。这类资源如果不被看成临界资源加以保护,那么很有可能因为访问冲突造成数据错乱的问题。
l 临界区:对于临界资源的访问,必须是互斥进行。也就是当临界资源被一个指令序列占用时,另一个需要访问相同临界资源的指令序列就不能被执行。我们知道这里所说的指令序列在现实中就都是作为进程/线程的一部分被OS调度执行的,指令序列不能执行的实际意思就是我们所说的其所在的进程/线程会被阻塞,直到其所申请的临界资源被释放。而程序内访问临界资源的代码序列被称为临界区。
l 互斥:是指同时只允许一个访问者对临界资源进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
l 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
2.2 Linux上对线程互斥和同步的支持方法。
| 互斥 | 同步 |
进程 | System V信号量 POSIX 信号量 线程锁共享(选讲) | System V信号量(常用于进程的同步,还不清楚是否可以用于线程) POSIX 信号量 |
线程 | pthread_mutex_t POSIX 信号量 | pthread_cond_t(选讲) POSIX 信号量 |
我们对线程的互斥主要介绍NPTL库自带的pthread_mutex_t,对线程的同步主要介绍基于POSIX信号量的机制。而且我们可以看到也可以用POSIX的信号量实现线程的互斥,但我们一般不会这么做,原因是NPTL自带的pthread_mutex_t的方式足够好,效率更高。
这里提到信号量怎么还有两套?什么是SystemV 信号量,什么是POSIX信号量?
信号量的概念是IPC机制的一个组成部分。这里先简单提一下就好。具体我们在讲到后面的进程间通信时会给大家做详细介绍。
参考:
1)浅谈进程同步和互斥的概念(比较经典,对临界区,同步,互斥的基本概念讲解比较好。建议有空再看看)
http://www.cnblogs.com/CareySon/archive/2012/04/14/Process-SynAndmutex.html
2)【操作系统笔记】同步与互斥的区别和联系(同步和互斥的概念讲得清楚)
http://blog.csdn.net/ns_code/article/details/17284351
3)进程互斥锁: http://blog.csdn.net/luansxx/article/details/7736618
4)Linux线程同步机制(原文是进程同步机制,但我认为应该是讲的线程)。文章对互斥,条件变量和信号量的缘来分析比较好,值得借鉴。
http://www.cnblogs.com/sooner/p/3191931.html
5)Linux的信号量总结:下面两个貌似重复
http://www.360doc.com/content/12/0723/00/9298584_225900606.shtml
http://wenku.baidu.com/view/9a91cd52ad02de80d4d840d8
后面调整一下顺序,先讲线程互斥,再讲线程同步。
3. Linux 线程互斥
重点:
l 线程库互斥量的使用
函数 | 说明 |
int pthread_mutex_init (pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
int pthread_mutex_destroy(pthread_mutex_t *mutex); | a. 静态初始化可以通过PTHREAD_MUTEX_INITIALIZER宏 pthread_mutex_t A = PTHREAD_MUTEX_INITIALIZER; b. 动态初始化可以通过pthread_mutex_init函数进行,一般在通过pthread_mutex_init初始化的锁,在不需要时应调用pthread_mutex_destroy销毁。
attr: 互斥锁属性 // NULL表示缺省属性 |
int pthread_mutex_lock(pthread_mutex_t *mutex) | 通过pthread_mutex_lock来获得锁 注意: 如果想获得的锁已经被别的线程获取了,此时pthread_mutex_lock将引起调用者阻塞 |
int pthread_mutex_unlock(pthread_mutex_t *mutex) | 通过pthread_mutex_unlock来释放获得的锁 |
举例:<<<<<ppt的例子P75-P78, 可以让学生先看看再讲
samples\2.2-Process\2.Threads\3-mutex\ pthread_mutex.c
打开_LOCK_,就不会出现打印否则因为不互斥会导致打印出现。>>>>>>>>>>>>>>>>
实验:<<<<<使用pthread mutex实现AB打印的互斥功能
samples\2.2-Process\2.Threads\1.5-mthreads\AB_mutex.c
屏幕上三行A或者三行B在一起显示,但AB的次序无要求 >>>>>>
4. Linux 线程同步
重点:
l 同步原语
l 理解信号量的概念
l 掌握使用信号量同步的方法
4.1 信号量的概念P64-P65
4.1.1 信号量的定义:
首先别把信号量和同步等同起来,正确的理解是信号量是一种操作系统上下文中采用原子方式检测-申请;释放共享资源的方法。我们要学习的是如何利用信号量来实现互斥和同步。
4.1.2 信号量的三种操作
1) 信号量的初始化:初始化就是指定该类共享资源的初始可用值,以后可以采用P操作和V操作进行改变。注,这里初始化的并不是信号量的限制值,只是初始值,后面可以V来增加配额。信号量的值不能 < 0,系统最大可用值为SEM_VALUE_MAX,见limits.h
2) P操作:
If (信号量的值 > 0) {
申请资源的任务继续;信号量值--;
} else {
申请资源的任务阻塞
}
3) V操作:
If (没有任务等待该资源) {
信号量++
} else {
唤醒第一个等待任务,让其运行。
}
但是我们要注意一点,一个信号量在申请时可能被阻塞,但在释放时一定不会阻塞。
举个踢足球的例子,一个操场上最多可以上的人数。
画一下PV操作过程中OS,进程之间的关系。
信号量可以用来支持软件中的同步和互斥的概念,当信号量的可用值退化为1时就是互斥。
我们这里讨论的是用户态的信号量,内核态还有内核态的信号量。
用户信号量又分POSIX信号量和system V信号量,systemV信号量我们在后面讲进程通信会用到,这里还有posix信号量又分为有名信与量和无名信号量,在线程这里我们这里重点介绍POSIX的无名信量。
4.2 POSIX 信号量API
要用这些函数都要加上头文件#include <semaphore.h>
信号量的数据类型为结构sem_t,它本质上是一个长整型的数
函数 | 说明 |
int sem_init (sem_t *sem, int pshared, unsigned int value) | l sem_init()初始化一个定位在sem的匿名信号量。value参数指定信号量的初始值。pshared参数指明信号量是由进程内线程共享,还是由进程之间共享。如果pshared的值为0,那么信号量将被进程内的线程共享,并且应该设置在所有线程都可以看见的地址上(如全局变量,或者堆上动态分配的变量)。 l 如果pshared是非零值,那么信号量将在进程之间共享,并且应该定位共享内存区域。因为fork()创建的孩子继承其父亲的内存映射,因此它也可以见到这个信号量。所有可以访问共享内存区域的进程都可以用sem_post、sem_wait操作信号量。 l 参数说明: ² sem :信号量对象 ² pshared:控制信号量的类型,0表示这个信号量时当前进程的局部信号量,否则,这个信号量就可以在多个进程间共享。 ² value:信号量的初始值(即资源的个数); |
int sem_wait(sem_t *sem) | P操作 sem_wait的作用是以原子操作的方式给信号量的值减1,但它会等到信号量非0时才会开始减法操作。如果此时信号量的值为0,这个函数就会等待,直到有线程增加了该信号量的值使其不再为0。 |
int sem_post(sem_t *sem) | V操作 sem_post的作用是以原子操作的方式给信号量的值加1。 |
举例 <<<<<<<<用信号量实现互斥的例子,这个有空可以让学生自己练习一下。
回忆前面的AB.c,如果我们想把A和B都在一起打印怎么互斥
samples\2.2-Process\2.Threads\4.2\sem_mutex.c<<<<<<<<<<<
举例:<<<<<利用信号量实现AB的同步。
上面的例程,到底哪个线程先申请到信号量资源,这是随机的。如果想要某个特定的顺序的话,可以用2个信号量来实现。例如下面的例程是线程1先执行完,然后线程2才继续执行,直至结束。
samples\2.2-Process\2.Threads\4.2\sem_sync.c >>>>>>>>>>
提高:<<<<PV经典问题C实现
http://blog.csdn.net/life_hunter/article/details/8790553
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>