QNX EasyStart chapter 1 :Processes and Threads

chapter 1 :Processes and Threads

Process and thread fundamentals

在开始讨论线程,进程,时间片以及所有其他精彩的“计划概念”之前,让我们建立一个类比。我首先要做的是说明线程和进程如何工作。我能想到的最好方法(缺少深入研究实时系统的设计)是在某种情况下想象我们的线程和过程。

A process as a house

让我们以一个日常的日常对象(一所房屋)为基础来进行流程和线程的类比。房屋实际上是具有某些属性(例如,地面空间量,卧室数量等)的容器。如果您以这种方式看待,房子实际上并不会主动做任何事情,而是被动对象。这实际上是一个过程。我们将很快对此进行探讨。

The occupants as threads

居住在房屋中的人是活跃的对象-他们是使用各个房间,看电视,做饭,洗澡的人。我们很快就会看到线程的行为。

Single threaded

如果您曾经独自生活过,那么您就知道这就像您知道自己可以随时在房子里做任何事情一样,因为房子里没有其他人。如果您想打开立体声音响,请使用洗手间,随便吃晚饭,只要继续就可以。

Multi threaded

当您将另一个人添加到房屋中时,情况会发生巨大变化。假设您已结婚,那么现在您也有一个夫妻住在那儿。您不能在任何给定的时间进入洗手间。您需要先检查以确保您的配偶不在其中!
如果您有两个负责任的成年人居住在房屋中,那么通常您对“安全性”会比较松懈-您知道另一个成年人会尊重您的空间,不会试图放火烧厨房(故意!),等等。上。现在,把几个孩子混在一起,突然之间事情变得更加有趣了。

Back to processes and threads

就像一间房屋占用不动产一样,过程也要占用内存。正如房屋的占用者可以自由进入他们想要的任何房间一样,进程的线程都可以对该内存进行通用访问。如果一个线程分配了某些东西(妈妈出去玩游戏),那么所有其他线程都可以立即访问它(因为它存在于公共地址空间中-它在房子里)。
同样,如果过程分配内存,此新内存也可用于所有线程。这里的窍门是识别内存是否应供进程中的所有线程使用。如果是这样,那么您将需要让所有线程同步它们对其的访问。如果不是,那么我们将假定它特定于特定线程。
在那种情况下,由于只有该线程可以访问它,因此我们可以假设不需要同步,该线程不会使自己跳闸!从日常生活中我们知道,事情并不是那么简单。现在,我们已经了解了基本特征(摘要:所有内容都是共享的),让我们看一下事情变得更有趣的地方以及原因。
下图显示了我们表示线程和进程的方式。进程是一个圆形,代表“容器”概念(地址空间),三个弯曲的线是螺纹。
DraggedImage.png
在整本书中,您都会看到类似的图表。如果您想洗个澡,并且已经有人在洗手间,则必须等待。线程如何处理呢?这可以通过互斥来完成。
这几乎意味着您的想法当涉及到特定资源时,许多线程是互斥的。
如果要洗个澡,想独享浴室。为此,您通常会进入浴室并从内部锁定门。任何试图使用浴室的人都会被锁锁住。完成后,您将打开门,允许其他人进入,就是线程的作用。
线程使用一个称为互斥对象的对象(互斥(MUTual EXclusion)的缩写)。
该对象就像门上的锁一样,一旦某个线程将互斥锁锁定,其他线程就无法获取该互斥锁,直到拥有线程释放(解锁)它为止。就像门锁一样,等待获取互斥量的线程将被禁止。互斥锁和门锁发生的另一个有趣的相似之处是,互斥锁实际上是“建议”锁。如果线程不遵循使用互斥锁的约定,则保护是无用的。在我们的房屋类比中,这就像有人通过其中一堵墙闯入洗手间而忽略了门和锁的惯例。

优先事项 Priorities

如果浴室当前处于锁定状态,并且有许多人在等待使用该怎么办?显然,所有的人都坐在外面,等着谁在浴室里出来。真正的问题是,“当门解锁时会发生什么?下一步谁去?”
您会认为,允许任何人等待最长时间的下一步是“公平的”。或者让最年长的人去下一步可能是“公平的”。或最高。或最重要的。有很多方法可以确定什么是“公平的”。我们通过两个因素通过线程解决此问题:优先级和等待时间假设两个人同时出现在(上锁的)浴室门上。其中一个有一个紧迫的截止日期(他们已经开会迟到了),而另一个没有。允许紧迫的最后期限的人再去是不是很有意义?好吧,当然可以。
唯一的问题是您如何确定谁更“重要”。这可以通过分配优先级来实现(让我们像Neutrino一样使用数字-此版本中,最低优先级是1,最高优先级是255)。
截止日期紧迫的人将被赋予较高的优先权,而没有截止日期的人将被赋予较低的优先权。与线程相同。线程从其父线程继承其调度策略。但可以(如果有权)调用pthread setschedparam()更改其调度策略和优先级,或调用pthread setschedprio()更改其优先级。
如果有多个线程正在等待,并且互斥锁被解锁,则我们会将互斥锁赋予具有最高优先级的等待线程。但是,假设两个人具有相同的优先级。现在你怎么办?
好吧,在这种情况下,允许等待时间最长的人继续前进是“公平的”。这不仅是“公平的”,而且也是Neutrino内核所做的。在等待一堆线程的情况下,我们首先要优先考虑,其次才是等待时间。互斥锁当然不是我们将遇到的唯一同步对象。
让我们看看其他。

Semaphores

让我们从浴室进入厨房,因为这是一个社会上可以接受的位置,可以同时容纳多个人。在厨房里,您可能不想一次让所有人都在厨房里。实际上,您可能想限制厨房里的人数(厨师太多,等等)。假设您永远不想同时有两个以上的人。你可以用互斥锁mutex吗?不是我们定义的那样。为什么不?对于我们的类比,这实际上是一个非常有趣的问题。
让我们将其分解为几个步骤。

A semaphore with a count of 1

浴室可以具有以下两种情况之一,两种状态相互关联:

  • 门是开锁的,房间里没人
  • 门锁着,一个人在房间里
    没有其他组合是不可能的-不能在房间里没人的情况下将门锁上(我们将如何解锁?),也不能和房间里的某人一起将门解锁(他们将如何确保他们的隐私?)。这是一个信号量为1的示例-该房间中最多只能有一个人,或者使用该信号量只有一个线程。这里的钥匙(对双关语)是我们表征锁的方式。
    在典型的浴室锁中,您只能从内部对其进行锁定和解锁-没有外部可访问的钥匙。实际上,这意味着互斥锁的所有权是一个原子操作–在您获取互斥锁的过程中,没有任何其他线程可以获取互斥锁的可能性,结果是您俩都拥有该互斥锁。在我们的房子里,这种比喻不太明显,因为人类比一和零聪明得多。
    厨房所需的是另一种类型的锁。

A semaphore with a count greater than 1

假设我们在厨房中安装了传统的基于钥匙的锁。此锁的工作方式是,如果您有钥匙,则可以解锁门并进去。使用此锁的任何人都同意,当他们进入室内时,他们会立即从内部锁定门,以便外面的任何人都可以。
总是需要一个钥匙。
好了,现在控制厨房中要多少人的事情变得很简单-在门外悬挂两个钥匙!厨房总是上锁的。当有人想要进入厨房时,他们会看到门外是否挂有钥匙。如果是这样,他们会随身携带,解锁厨房门,走进去,然后使用钥匙锁定门。由于进入厨房的人在厨房时必须随身带钥匙,因此我们通过限制门外钩子上可用钥匙的数量来直接控制在任何给定位置允许进入厨房的人数。
门。
对于线程,这是通过信号量来完成的。 “普通”信号灯的工作方式与互斥锁一样,您可以拥有互斥锁(在这种情况下您可以访问资源),也可以不拥有互斥锁(在这种情况下您无法访问资源)。我们刚才在厨房中描述的信号量是一个计数信号量-它跟踪计数(通过线程可用的键数)。

A semaphore as a mutex

我们只是问了一个问题:“可以用互斥锁mutex吗?”关于实现带计数的锁,答案是否定的。反过来呢?
我们可以使用信号量semaphore作为互斥量mutex吗?是。实际上,在某些操作系统中,它们正是这样做的-它们没有互斥量,只有信号量!那么,为什么还要烦恼互斥体呢?
要回答这个问题,请查看您的洗手间。您房屋的建造者如何实施“互斥体”?我怀疑你没有钥匙挂在墙上!
互斥体是“特殊目的”的信号量。Mutexes are a “special purpose” semaphore.如果希望一个线程在代码的特定部分中运行,则互斥锁是迄今为止最有效的实现。稍后,我们将介绍其他同步方案-称为condvars,barrier和sleepon的事物。
things called condvars, barriers, and sleepons.如此一来,就不会感到困惑,意识到互斥体还有其他属性,例如优先级继承,可以将其与信号量区分开。

The kernel’s role

房屋类比非常适合用于理解同步的概念,但是它属于一个主要领域。在我们的房子里,我们有许多线程同时运行。但是,在实际的实时系统中,通常只有一个CPU,因此一次只能运行一个“事物thing”。

Single CPU

让我们看一下实际情况,特别是在“经济”情况下,系统中有一个CPU。在这种情况下,由于仅存在一个CPU,因此在任何给定时间点只能运行一个线程。内核决定(使用许多规则,我们将很快看到)运行哪个线程,然后运行它。

Multiple CPU (SMP)

如果购买的系统具有多个共享内存和设备的相同CPU,则将有一个SMP框(SMP代表“对称多处理器”,“对称”部分表示系统中的所有CPU均相同)。在这种情况下,可以同时(同时)运行的线程数受CPU数量的限制。 (实际上,单处理器盒也是如此!)由于每个处理器一次只能执行一个线程,而对于多个处理器,则可以同时执行多个线程。
现在让我们忽略当前存在的CPU数量-一个有用的抽象是将系统设计为好像多个线程实际上同时在运行,即使事实并非如此。稍后,在“使用SMP时需要注意的事项”部分,我们将看到SMP的一些非直观影响。

The kernel as arbiter 仲裁者

那么,谁决定哪个线程将在任何给定的时间点运行?那是内核的工作。内核确定在特定时刻应该使用哪个线程的CPU,并将上下文切换到该线程。让我们检查一下内核对CPU的作用。CPU具有许多寄存器(确切的数目取决于处理器系列,例如x86与MIPS,以及特定的家族成员,例如80486与Pentium)。
当线程运行时,信息存储在那些寄存器中(例如,当前程序位置)。当内核决定另一个线程应该运行时,它需要:

  • 保存当前正在运行的线程的寄存器和其他上下文信息
  • 将新线程的寄存器和上下文加载到CPU中
    但是内核如何决定另一个线程应该运行?此时将检查特定线程是否能够使用CPU。
    例如,当我们谈论互斥锁时,我们引入了阻塞状态(当一个线程拥有该互斥锁,而另一个线程也想要获取该互斥锁时,就会发生这种状态;第二个线程将被阻塞)。
    因此,从内核的角度来看,我们有一个线程可以消耗CPU,而另一个线程则不能,因为它被阻塞了,等待一个互斥体。在这种情况下,kerne允许可以运行的线程消耗CPU,并将另一个线程放入内部列表中(以便内核可以跟踪其对互斥锁的请求)。
    显然,这不是一个非常有趣的情况。假设有多个线程可以使用CPU。还记得我们根据优先级和等待时间委派访问互斥锁吗?内核使用类似的方案来确定下一个要运行的线程。
    有两个因素:优先级和调度策略,按该顺序进行评估。
    priority and scheduling policy, evaluated in that order

Prioritization 优先次序

考虑两个可以使用CPU的线程。如果这些线程具有不同的优先级,那么答案确实非常简单-内核将CPU分配给最高优先级的线程。正如我们在谈到获取互斥锁时提到的那样,Neutrino的优先级从一个(可用的最低)开始。
请注意,优先级零是为空闲线程保留的,您不能使用它。 (如果您想知道系统的最小值和最大值,请使用sched get priority min()sched get priority_max()的功能-它们是在<sched.h>中原型化的。在本书中,我们假设1个是最低的可用地址,而255个是最高的可用地址。)
如果另一个具有更高优先级的线程突然变得可以使用CPU,则内核将立即上下文切换到更高优先级的线程。我们将其称为抢占-优先级较高的线程优先于优先级较低的线程。当较高优先级的线程完成并且内核上下文切换回之前运行的较低优先级的线程时,我们称为此恢复-内核恢复运行先前的线程。
现在,假设两个线程能够使用CPU并具有完全相同的优先级。

Scheduling policies 计划政策

假设其中一个线程当前正在使用CPU。在这种情况下,我们将检查内核用来决定何时进行上下文切换的规则。 (当然,整个讨论实际上仅适用于优先级相同的线程-优先级高的线程准备使用CPU时就可以获取它;这就是在实时操作系统中拥有优先级的全部要点。)
Neutrino内核可理解的两个主要调度策略scheduling policies(策略)是Round Robin(或简称为“ RR”)和FIFO(先进先出)。 (虽然还有零星的调度,但是这超出了本书的范围;请参见《系统架构指南》的“ QNX Neutrino微内核”一章中的“零星调度”。)

FIFO

在FIFO调度策略中,允许线程消耗CPU所需的时间。这意味着,如果该线程正在进行很长的数学计算,并且没有其他优先级更高的线程就绪,则该线程可能会永远崩溃。相同优先级的线程呢?
他们也被锁定。 (在这一点上很明显,较低优先级的线程也被锁定。)如果正在运行的线程退出或自愿放弃了CPU,则内核会以能够使用CPU的相同优先级查找其他线程。
如果没有此类线程,则内核会寻找能够使用CPU的低优先级线程。请注意,术语“自愿放弃CPU”可以表示以下两种情况之一。
如果线程进入睡眠状态,或者阻塞了信号灯等,那么可以,较低优先级的线程可以运行(如上所述)。但是,还有一个“特殊的”调用,基于内核调用SchedYield()的sched yield(),它仅将CPU放弃给具有相同优先级的另一个线程-较低优先级的线程将永远不会有机会运行更高优先级已准备就绪。如果某个线程实际上确实调用了sched yield(),并且没有其他优先级相同的线程可以运行,则原始线程继续运行。有效地,使用sched yield()可以使另一个优先级相同的线程在CPU。
在下图中,我们看到三个线程在两个不同的进程中运行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2hpfG69-1584430916983)(DraggedImage-1.png)]
如果我们假设线程“ A”和“ B”处于就绪状态,并且线程“ C”被阻塞(可能正在等待互斥锁),并且线程“ D”(未显示)当前正在执行,那么这就是
Neutrino内核维护的READY队列的一部分如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZW8Fki7M-1584430916984)(DraggedImage-2.png)]
这显示了内核的内部READY队列,内核用来决定下一步调度谁。请注意,线程“ C”不在READY队列中,因为它已被阻塞,线程“ D”也不在READY队列中,因为它正在运行。

Round Robin

RR调度策略与FIFO相同,不同之处在于,如果存在另一个具有相同优先级的线程,则该线程不会永远运行。它仅针对系统定义的时间片运行,您可以使用该函数sched_rr_get_interval()确定其值。
时间片通常为4毫秒,但实际上是滴答大小的4倍 The timeslice is usually 4 ms, but it’s actually 4 times the ticksize,您可以使用ClockPeriod()查询或设置它。
发生的情况是内核启动了RR线程并记录了时间。如果RR线程运行了一段时间,则分配给它的时间将结束(时间片将过期)。内核将查看是否有另一个具有相同优先级的线程已经准备就绪。如果存在,则内核将运行它。如果不是,则内核将继续运行RR线程(即,内核授予该线程另一个时间片)。

The rules

让我们按重要性顺序总结调度规则(针对单个CPU):一次只能运行一个线程。

  • 优先级最高的就绪线程将运行。
  • 线程将一直运行直到阻塞或退出。
  • RR线程将为其时间片运行,然后内核将对其进行重新调度(如果需要)。
    以下流程图显示了内核做出的决定:
    DraggedImage-3.png
    DraggedImage-4.png
    DraggedImage-5.png
    对于多CPU系统,规则是相同的,只是多个CPU可以同时运行多个线程。线程的运行顺序(即,哪些线程可以在多个CPU上运行)的确定方式与单个CPU完全相同,优先级最高的READY线程将在CPU上运行。对于低优先级或等待时间更长的线程,内核对于何时安排它们具有一定的灵活性,以避免使用缓存时效率低下。有关SMP的更多信息,请参见《多核处理用户指南》。

Kernel states

我们一直在松散地谈论“运行”,“就绪”和“阻塞”——现在让我们正式化formalize这些线程状态。

RUNNING

Neutrino的RUNNING状态只是意味着该线程现在正在积极消耗CPU。在SMP系统上,将运行多个线程。在单处理器系统上,将运行一个线程。

READY

READY状态表示此线程可以立即运行-除非当前运行,否则,因为另一个线程(优先级相同或更高)正在运行。如果两个线程能够使用CPU,则一个优先级为10的线程和一个优先级为7的线程,优先级10的线程将为RUNNING,优先级7的线程将为READY。

The blocked states

我们称之为封锁状态?问题是,不仅存在一种阻塞状态。在Neutrino领导下,实际上有十几个blocking states
为什么那么多?因为内核跟踪线程被阻止的原因。我们已经看到了两个阻塞状态-当一个线程被阻塞等待互斥时,when a thread is blocked waiting for a mutex,该线程处于MUTEX状态。当线程被阻塞等待信号量时,它处于SEM状态。
这些状态仅指示线程被阻塞在哪个队列(和哪个资源)上。如果多个线程在互斥锁上处于阻塞状态(处于MUTEX阻塞状态),则直到拥有该互斥锁的线程将其释放之前,它们不会从内核引起注意。到那时,已阻塞的线程之一已准备就绪,内核做出了重新调度的决定(如果需要)。
为什么“如果需要”?刚释放互斥锁的线程可能还有其他事情要做,并且比等待线程的优先级更高。在这种情况下,我们转到第二条规则,该规则指出“将运行最高优先级的就绪线程”,这意味着调度顺序未更改-较高优先级的线程继续运行。

Kernel states, the complete list

这是内核阻塞状态的完整列表,并简要说明了每个状态。顺便说一句,该列表在<sys / neutrino.h>中可用-您会注意到状态都以STATE为前缀(例如,此表中的“ READY”在头文件中列为STATE READY):
DraggedImage-6.png
DraggedImage-7.png
要记住的重要一点是,当线程被阻塞时,无论其处于阻塞状态是什么,它都不会占用CPU。相反,线程消耗CPU的唯一状态是RUNNING状态。我们将在“消息传递”一章中看到“发送”,“接收”和“答复”阻止状态。 We’ll see the SEND, RECEIVE, and REPLY blocked states in the Message Passing。
NANOSLEEP状态与sleep()之类的函数一起使用,我们将在“时钟,计时器”一章中介绍该状态。
INTR状态与InterruptWait()一起使用,我们将在“中断”一章中进行介绍。
本章讨论了其他大多数状态。

Threads and processes

让我们从真实的实时系统的角度回到对线程和进程的讨论。然后,我们来看一下用于处理hread和进程的函数调用。我们知道一个进程可以有一个或多个线程。 (一个零零散度的进程将无法做任何事情,可以说实际上没有人在家做任何有用的工作。)Neutrino系统可以具有一个或多个进程。 (相同的讨论适用于-具有零个进程的Neutrino系统将无济于事。)
那么这些进程和线程是做什么的呢?最终,它们形成一个系统-执行某些目标的线程和进程的集合。在最高级别上,系统由许多过程组成。每个进程负责提供某种性质的服务-无论是文件系统,显示驱动程序,数据获取模块,控制模块还是其他。在每个进程中,可能有多个线程。线程数有所不同。
仅使用一个线程的设计者可以完成与使用五个线程的另一设计者相同的功能。有些问题使自己成为多线程的,并且实际上相对容易解决,而其他进程则使自己倾向于peing单线程,并且很难使多线程。使用线程进行设计的主题很容易占据另一本书——我们在这里只坚持基础知识。

Why processes?

那么,为什么不仅仅拥有一个拥有无数线程的进程呢?尽管某些操作系统迫使您采用这种方式进行编码,但将事物分解为多个进程的好处却很多:

  • 去耦和模块化
  • 可维护性
  • 可靠性
    将问题“分解”为几个独立问题的能力是一个强大的概念。它也是Neutrino的核心。中微子系统由许多独立的模块组成,每个模块都有一定的责任。这些独立的模块是不同的过程。 QSS的人员使用此技巧来孤立地开发模块,而无需模块相互依赖。模块之间相互之间唯一的“依赖”是通过少量定义明确的接口。
    由于缺乏相互依赖性,因此自然可以提高可维护性。由于每个模块都有自己的特定定义,因此修复一个模块相当容易-尤其是因为它不与任何其他模块绑定,但是,可靠性也许是最重要的一点。就像房子一样,过程也有一些定义明确的“边界”。一个房子里的人有一个很好的主意他们在房子里,不在的时候。
    线程有一个很好的主意-如果它在进程中访问内存,则它可以存活。如果它超出了进程地址空间的范围,它将被杀死。If it steps out of the bounds of the process’s address space, it gets killed.这意味着在不同进程中运行的两个线程实际上是相互隔离的。
    DraggedImage-8.png
    进程地址空间由Neutrino的进程管理器模块维护和执行。启动进程后,进程管理器会为其分配一些内存并启动线程运行。内存被标记为由该进程拥有。
    这意味着如果该进程中有多个线程,并且内核需要在它们之间进行上下文切换,那么这是一个非常高效的操作——我们不必挂起地址空间,只需运行哪个线程即可。但是,如果必须在另一个进程中切换到另一个线程,则进程管理器会介入并导致地址空间切换。不用担心,尽管在此附加步骤中还有更多的开销,但是在Neutrino下,这仍然非常快。

Starting a process

现在,我们将注意力转移到可用于处理线程和进程的函数调用上。任何线程都可以启动进程。唯一施加的限制是源自基本安全性的限制(文件访问,特权限制等)。您极有可能已经开始了其他过程。从系统启动脚本,外壳程序,或者让一个程序代表您启动另一个程序。

Starting a process from the command line

例如,您可以在shell中输入:

$ program1

这指示shell程序启动一个名为program1的程序,并等待它完成,或者,您可以键入:

$ program2 &

这指示shell启动program2,而无需等待它完成。我们说program2在“后台”运行。如果要在启动程序之前调整程序的优先级,则可以使用nice命令,就像在UNIX中一样:

$ nice program3

这指示外壳程序以降低的优先级启动program3。
还是呢?如果您看一下实际发生的情况,我们告诉Shell以常规优先级运行一个名为nice的程序。 nice命令将其优先级调整为较低(这是名称“ nice”的来源),然后以较低优先级运行program3。

Starting a process from within a program

您通常不关心Shell创建进程的事实-这是关于Shell的基本假设。在某些应用程序设计中,您肯定会依靠Shell脚本(文件中的命令批次)来为您完成工作,但是在其他情况下,您将需要自己创建进程。
例如,在大型的多进程系统中,您可能希望让一个主程序基于某种配置文件kind of configuration file为您的应用程序启动所有其他进程。另一个示例将包括在检测到某些操作条件(事件)时启动过程。
让我们看一下Neutrino提供的用于启动其他进程(或转换为其他程序)的功能:
DraggedImage-9.png
您使用哪种功能取决于两个要求:可移植性和功能。
像往常一样,两者之间需要权衡。以下是在创建新流程的所有调用中发生的常见事件。原始进程中的线程调用上述函数之一。
最终,该功能使流程管理器为新流程创建地址空间。然后,内核在新进程中启动线程。该线程执行一些指令,并调用main()。 (当然,对于fork()和vfork(),新线程通过从fork()或vfork()返回而开始在新进程中执行;我们将在短期内看到如何处理。)

Starting a process with the system() call

system()函数是最简单的;它需要一个命令行,与在shell提示符下键入的命令行相同,然后执行它。实际上,system()实际上启动了一个外壳程序来处理您要执行的命令。我用来编写本书的编辑器利用了system()调用。在编辑时,我可能需要“掏空”,签出一些样本,然后回到编辑器中,而又不会丢失我的位置。在此编辑器中,我可以发出命令:例如pwd来显示当前的工作目录。编辑器为:! pwd命令:

system ("pwd");

system()是否适合所有事物?当然不是,但这对于您的许多过程创建需求很有用。

Starting a process with the exec() and spawn() calls

让我们看一下其他一些流程创建函数。我们应该看的下一个过程创建函数是exec()和spawn()系列。在详细介绍之前,让我们看一下这两组函数之间的区别。exec()系列将当前进程转换为另一个进程。我的意思是,当一个进程发出exec()函数调用时,该进程将停止运行当前程序并开始运行另一个程序。进程ID不会更改,该进程已更改为另一个程序。
进程中的所有线程都发生了什么?当我们查看fork()时,我们将回到这一点。另一方面,spawn()系列却不这样做。
调用spawn()系列的成员会创建另一个进程(具有新的进程ID),该进程与函数的参数中指定的程序相对应。让我们看一下spawn()和exec()函数的不同变体。在下面的表中,您将看到哪些是POSIX,哪些不是。当然,为了获得最大的可移植性,您将只想使用POSIX函数。
DraggedImage-10.png
DraggedImage-11.png
尽管这些变体看起来不堪重负,但它们的后缀却有一个模式:
DraggedImage-12.png
参数列表是传递给程序的命令行参数列表。
另外,请注意,在C库中,spawnlp(),spawnvp()和spawnlpe()都是cal spawnvpe(),它们依次调用spawnp()。函数spawnle(),spawnv()和spawnl()最终都调用spawnve(),然后又调用spawn()。最后,spawnp()调用spawn()。因此,所有生成功能的根源是spawn()调用。现在,让我们详细了解各种spawn()和exec()变体,以便您可以体会到所使用的各种后缀。然后,我们将看到spawn()调用本身。例如,如果我想使用参数-t,-r和-1调用ls命令(意思是“按时间排序输出,并以相反的顺序显示输出的长版本”),则可以指定为:

/* To run ls and keep going: */
spawnl (P_WAIT, "/bin/ls", "/bin/ls", "-t", "-r", "-l", NULL);
/* To transform into ls: */
execl ("/bin/ls", "/bin/ls", "-t", "-r", "-l", NULL);
or, using the v suffix variant:
char *argv [] =
{
    "/bin/ls",
    "-t",
    "-r",
    "-l",
NULL };
/* To run ls and keep going: */ spawnv (P_WAIT, "/bin/ls", argv);
/* To transform into ls: */ execv ("/bin/ls", argv);

(略)46

Starting a thread

现在我们已经了解了如何启动另一个进程,让我们看看如何启动另一个线程。任何线程都可以在同一进程中创建另一个线程。没有任何限制(当然,内存空间不足!)。最常见的方法是通过POSIX pthread create()调用:

#include <pthread.h>
int
pthread_create (pthread_t *thread,
const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

DraggedImage-13.png
请注意,线程指针和属性结构(attr)是可选的-您可以将它们作为NULL传递。thread参数可用于存储新创建的线程的线程ID。
您会注意到,在下面的示例中,我们将传递NULL,这意味着我们不在乎新创建的线程的ID。如果我们确实在意,我们可以做这样的事情:

pthread_t tid;
pthread_create (&tid, ...
printf ("Newly created thread id is %d\n", tid);

这种用法实际上是非常典型的,因为您经常想知道哪个线程ID正在运行哪个代码段。一个小妙点。新创建的线程可能在填充线程ID(tid参数)之前正在运行。这意味着您应谨慎使用tid作为全局变量。上面显示的用法还可以,因为返回了pthread create()调用,这意味着tid值已正确填充。

The thread attributes structure 线程属性结构

启动新线程时,它可以采用一些定义明确的默认值,也可以显式指定其特征。在讨论线程属性函数之前,让我们看一下pthread_attr_t数据类型:
DraggedImage-14.png
(略)54
这看起来像一个很大的列表(20个函数),但实际上我们只需要担心其中一半,因为它们是配对的:“ get”和“ set”(pthread attr init除外)和pthread_attr_destroy()
在检查属性函数之前,需要注意一件事。您必须先调用pthread attr init()初始化属性结构,然后使用适当的pthread attr set ()函数对其进行设置,然后调用pthread create()创建线程。创建线程后更改属性结构无效。

线程属性管理

在使用函数pthread attr init()之前必须先对其进行初始化:

...
pthread_attr_t attr;
...
pthread_attr_init (&attr);

您可以调用pthread attr destroy()来“初始化”线程属性结构,但是几乎没有人这么做(除非您拥有POSIX兼容的代码)。在下面的描述中,我将默认值标记为“(默认)”。

“标志”线程属性

pthread attr setdetachstate(),pthread attr setinheritsched()和pthread attr setscope()这三个函数确定线程是“可连接的”还是“分离的”创建的,线程是继承创建线程的调度属性还是使用线程。调度由pthread attr setschedparam()和pthread attr setschedpolicy()指定的属性,最后调度线程的作用域是“系统”还是“进程”。要创建“可连接”线程(意味着另一个线程可以通过pthread join()与其终止同步,请使用:

(default)
pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);

要创建一个不能加入的线程(称为“分离”线程),可以使用:

pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);

如果您希望线程继承创建线程的调度属性(即具有相同的调度策略和相同的优先级),则可以使用:

(default)
pthread_attr_setinheritsched (&attr, PTHREAD_INHERIT_SCHED);

要创建一个使用属性结构本身中指定的调度属性(您可以使用pthread_ attr_setschedparam()和pthread attr setschedpolicy()),您可以使用:

pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED);

最后,您永远不会调用pthread attr setscope()。
为什么?
由于Neutrino仅支持“系统”作用域,因此是初始化属性时的默认设置。 (“系统”作用域意味着系统中的所有线程相互争夺CPU;另一个值“进程”意味着进程中的线程彼此争夺CPU,并且内核调度进程。)如果您确实坚持要调用它,则只能按以下方式调用它:


(default)
pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM);

(略)

A few examples

让我们看一些例子。我们假定已经包含了正确的包含文件(<pthread.h>和<sched。h>),并且要创建的线程称为新线程),并且已正确地原型化和定义了。创建线程的最常见方法是简单地将值设为默认值:

pthread_create (NULL, NULL, new_thread, NULL);

在上面的示例中,我们使用默认值创建了新线程,并向其传递了NULL作为其唯一的参数(这是上面pthread create()调用中的第三个NULL)。
通常,您可以将所需的任何内容(通过arg字段)传递给新线程。在这里,我们传递数字123:

pthread_create (NULL, NULL, new_thread, (void *) 123);

一个更复杂的示例是使用优先级15的循环调度创建一个不可连接的线程:

		pthread_attr_t attr;
		// initialize the attribute structure 
		pthread_attr_init (&attr);
		// set the detach state to "detached"
		pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
		// override the default of INHERIT_SCHED 
		pthread_attr_setinheritsched (&attr, PTHREAD_EXPLICIT_SCHED); 
		pthread_attr_setschedpolicy (&attr, SCHED_RR); 
		attr.param.sched_priority = 15;
		// finally, create the thread
		pthread_create (NULL, &attr, new_thread, NULL);

要查看多线程程序的外观,可以从shell程序运行pidin命令。假设我们的程序称为spud。如果我们在spud创建线程之前运行一次pidin,而在spud创建两个线程之后运行一次(总共三个) ,这就是输出的样子(我将pidin输出缩短​​为仅显示spud):
DraggedImage-15.png
如您所见,进程spud(进程ID 12301)具有三个线程(在“ tid”列下)。这三个线程通过循环调度算法(优先级10之后的“ r”表示)以优先级10运行。这三个线程都为READY,这意味着它们能够使用CPU,但当前不在CPU上运行(另一个优先级更高的线程,当前正在运行)。既然我们已经了解创建线程的全部知识,那么让我们看一下如何使用它们以及在何处使用它们。

Where a thread is a good idea

有两类问题,线程的应用是一个好主意。线程就像C加加中的重载运算符一样(在当时)将每个单个运算符进行一些有趣的用法重载似乎是一个好主意,但这会使代码难以理解。
与线程类似,您可以创建大量线程,但是额外的复杂性将使您的代码难以理解,因此难以维护。另一方面,明智地使用线程将导致代码在功能上非常干净。
线程很棒,您可以在其中并行化操作-想到许多数学问题(图形,数字信号处理等)。如果您希望程序在共享数据的同时执行几个独立的功能,例如在同时为多个客户端提供服务的Web服务器上,线程也非常有用。
我们将研究这两个类。

Threads in mathematical operations

假设我们有一个执行光线跟踪的图形程序。屏幕上的每条光栅线均取决于主数据库(该数据库描述了所生成的实际图片)。这里的关键是:每条栅格线彼此独立。这立即导致该问题作为线程程序脱颖而出。
这是单线程版本:
DraggedImage-16.png
在这里,我们看到该程序将对要计算的所有栅格线进行xl迭代。在SMP系统上,该程序将仅使用一个CPU。为什么?因为我们还没有告诉操作系统并行执行任何操作。操作系统不够智能,无法查看程序并说:“嘿,等一下!我们有4个CPU,看起来这里有独立的执行流程。我将在所有4个CPU上运行它! ”
因此,由系统设计人员(您)告诉Neutrino哪些零件可以并行运行。
最简单的方法是:
DraggedImage-17.png
这种简单的方法存在许多问题。
首先(这是最次要的),必须修改do_one_line()函数以采用void*而不是int作为其参数。使用atypecast可以很容易地对此进行补救。第二个问题有些棘手。
假设您正在计算图片的屏幕分辨率为1280 x1024。我们将创建1280个线程!
对于Neutrino来说,这不是问题-Neutrino将您“限制”到每个进程32767个线程!但是,每个线程必须具有唯一的堆栈。如果堆栈大小合理(例如8 KB),则将使用1280×8 KB(10兆字节!)的堆栈。
但是 SMP系统中只有4个处理器。这意味着1280个线程中只有4个同时运行-其他1276个线程正在等待CPU。 (实际上,堆栈将“故障转移”,这意味着仅根据需要分配其空间。尽管如此,这很浪费,仍然有其他开销。)
更好的解决方案是将问题分成4个部分(每个CPU一个),并为每个部分启动一个线程:

int num_lines_per_cpu;
int num_cpus;
int
main (int argc, char **argv)
{
	int cpu; 
...
    // perform initializations
	// get the number of CPUs
	num_cpus = _syspage_ptr -> num_cpu; 
	num_lines_per_cpu = num_x_lines / num_cpus; 
	for (cpu = 0; cpu < num_cpus; cpu++) {
			pthread_create (NULL, NULL,do_one_batch, (void *) cpu);
	}
} ...

   // display results
void * do_one_batch (void *c) 
{
		int cpu = (int) c;
        int x1;
		for (x1 = 0; x1 < num_lines_per_cpu; x1++)
		 { 
			do_line_line (x1 + cpu * num_lines_per_cpu);
		} 
}

在这里,我们仅启动num个cpus线程。每个线程将在一个CPU上运行。而且由于我们只有少数几个线程,所以我们不会在内存中浪费不必要的堆栈。注意,我们如何通过取消引用“系统页面”全局变量syspage ptr来获得CPU的数量。 (有关系统页面中内容的更多信息,请参阅QSS的《 Building Embedded Systems》一书或<sys / syspage.h>包含文件)。

Coding for SMP or single processor

关于此代码的最好的部分是,它将在单处理器系统上正常工作-您将只创建一个线程,并使其完成所有工作。额外的开销(一个堆栈)非常值得在SMP机器上让软件“更快地工作”的灵活性。

Synchronizing to the termination of a thread

我提到最初显示的简单代码示例存在许多问题。另一个问题是main()启动了一堆线程,然后显示结果。函数如何知道何时可以安全地显示结果?
具有main()函数轮询以完成操作会破坏实时操作系统的目的

int
main (int argc, char **argv)
{
...
    // start threads as before
	while (num_lines_completed < num_x_lines) 
	{
	 	sleep (1);
	} 
}

甚至不要考虑编写这样的代码!有两种优雅的解决方案:pthread join()和pthread barrier wait()

Joining

最简单的同步方法是在线程终止时加入线程。加入实际上意味着等待终止。连接是通过一个线程等待另一线程终止来完成的。等待线程调用pthread join():

#include <pthread.h>
int
pthread_join (pthread_t thread, void **value_ptr);

要使用pthread_join(),请向其传递您希望加入的线程的线程ID,以及一个可选值ptr,该值可用于存储加入的线程的终止返回值。
(如果您对此值不感兴趣,则可以传入NULL-在这种情况下,我们不需要。)线程ID是从哪里来的?我们在pthread create()中忽略了它-我们为第一个参数传递了NULL。现在让我们更正代码:

int num_lines_per_cpu, num_cpus;
int main (int argc, char **argv)
{
    int cpu;
    pthread_t *thread_ids;
    ... // perform initializations
    thread_ids = malloc (sizeof (pthread_t) * num_cpus);
    num_lines_per_cpu = num_x_lines / num_cpus;
    for (cpu = 0; cpu < num_cpus; cpu++)
    {
        pthread_create (&thread_ids [cpu], NULL, do_one_batch, (void *) cpu);
    }
// synchronize to termination of all threads 
    for (cpu = 0; cpu < num_cpus; cpu++) {
        pthread_join (thread_ids[cpu], NULL);
    }
    ...
// display results
}

您会注意到,这次我们将第一个参数传递给pthread create()作为指向pthread_t的指针。这是新创建线程的线程ID的存储位置。在第一个for循环完成后,我们将运行num个cpus线程,以及正在运行main()的线程。我们不太担心main()线程消耗了我们所有的CPU;它会花时间等待。
等待是通过依次对每个线程执行pthread join()来完成的。首先,我们等待thread_ids [0]完成。完成后,pthread join()将解除阻塞。 for循环的下一次迭代将使我们等待所有id cpus线程的thread_ids [1]完成,依此类推num_cpus线程。
此时出现的一个常见问题是:“如果线程以相反的顺序结束怎么办?”换句话说,如果有4个CPU,并且由于某种原因,在最后一个CPU(CPU 3)上运行的线程首先完成,然后在CPU 2上运行的线程接下来完成,以此类推呢?好吧,该方案的优点在于不会发生任何不良情况。
即将发生的第一件事是pthread join()将阻塞线程ID 0。同时,线程ID 3完成。这对main()线程绝对没有影响,该线程仍在等待第一个线程完成。然后,线程ID 2完成。仍然没有影响。依此类推,直到最后完成线程ID 0,此时,pthread join()解除阻塞,然后我们立即进行for循环的下一个迭代。 for循环的第二次迭代在线程ID 11上执行pthread join(),它将不会阻塞-它会立即返回。
为什么?
因为由线程ID 1标识的线程已经完成。因此,我们的for循环将“鞭打”其他线程,然后退出。到那时,我们知道已经与所有计算线程同步,因此现在可以显示结果。

Using a barrier

当我们谈到main()函数与工作线程完成之间的同步时(在上面的“与线程的终止同步中”),我们提到了两个方法:pthread join()和barrier,我们已经研究过。
回到我们的房屋类比,假设这个家庭想在某个地方旅行。驾驶员上车,然后启动引擎。并等待。司机等到所有家庭成员都登上车,然后货车才出发去旅行-我们不能把任何人抛在后面!
这正是图形示例所发生的情况。主线程需要等待,直到所有辅助线程都已完成,然后才能开始程序的下一部分。但是,请注意一个重要的区别。使用pthread join(),我们正在等待线程终止。这意味着线程不再与我们在一起。他们已经退出了。有了障碍,我们正在等待一定数量的线程在障碍处集合。然后,当必要的数字出现时,我们将全部解锁。 (请注意,线程继续运行。)
首先使用pthread barrier init()创建一个屏障:

#include <pthread.h>
int
pthread_barrier_init (pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr, unsigned int count);

这将在传递的地址处创建一个屏障对象(该屏障对象的指针位于barrier中),并具有attr指定的属性(我们将使用NULL来获取默认值)。必须在计数中传递必须调用pthread barrier wait()的线程数。创建屏障后,我们希望每个线程都调用pthread barrier wait()来表明它已完成:

#include <pthread.h>
int
pthread_barrier_wait (pthread_barrier_t *barrier);

当线程调用pthread barrier wait()时,它将阻塞,直到在pthread barrier init()中最初指定的线程数调用pthread barrier wait)为止(并且也阻塞了)。调用正确数量的线程后 pthread barrier wait(),所有这些线程将“同时simultaneously”解除阻塞。
Here’s an example:


/*
 *  barrier1.c
*/
#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <sys/neutrino.h>
pthread_barrier_t barrier; // the barrier synchronization object
void *
thread1 (void *not_used)
{
    time_t  now;
    char    buf [27];
    time (&now);
    printf ("thread1 starting at %s", ctime_r (&now, buf));
    // do the computation
    // let’s just do a sleep here...
    sleep (20);
    pthread_barrier_wait (&barrier);
    // after this point, all three threads have completed.
    time (&now);
    printf ("barrier in thread1() done at %s", ctime_r (&now, buf));
}
void *
thread2 (void *not_used)
{
    time_t  now;
    char    buf [27];
    time (&now);
    printf ("thread2 starting at %s", ctime_r (&now, buf));
    // do the computation
    // let’s just do a sleep here...
    sleep (40);
    pthread_barrier_wait (&barrier);
    // after this point, all three threads have completed.
    time (&now);
    printf ("barrier in thread2() done at %s", ctime_r (&now, buf));
}
main () // ignore arguments
{
    time_t  now;
    char    buf [27];
// create a barrier object with a count of 3 pthread_barrier_init (&barrier, NULL, 3);
// start up two threads, thread1 and thread2 pthread_create (NULL, NULL, thread1, NULL); pthread_create (NULL, NULL, thread2, NULL);
// at this point, thread1 and thread2 are running
// now wait for completion
    time (&now);
    printf ("main () waiting for barrier at %s", ctime_r (&now, buf)); pthread_barrier_wait (&barrier);
// after this point, all three threads have completed.
    time (&now);
    printf ("barrier in main () done at %s", ctime_r (&now, buf));
}

主线程创建了屏障对象,并使用“突破”之前应同步到屏障的线程数(包括自身!)对其进行了初始化。在我们的示例中,这是3(主线程),一个线程1(线程)和一个线程2()线程的计数。然后,像以前一样启动图形计算线程(在本例中为thread1()和thread2())。为了说明起见,我们没有显示图形计算的源,而是停留在睡眠中(20);和sleep(40);造成延迟,就好像正在进行计算一样。
要同步,主线程只是简单地将自己阻塞在屏障上,因为知道屏障只有在工作线程也加入屏障后才会解除阻塞。如前所述,使用pthread join(),工作线程完成并且死掉,以便主线程与其同步。但是有了障碍,线程仍然可以正常运行。实际上,当所有步骤都完成后,它们才刚刚从pthread屏障等待中释放出来。
这里引入的wrinkle是您应该准备好使用这些线程!在我们的图形示例中,他们无事可做(如我们所写)。在现实生活中,您可能希望开始进行下一帧计算。

Multiple threads on a single CPU

假设我们稍微修改一下示例,以便可以说明为什么即使在单CPU系统上也具有多个线程有时也是一个好主意。在此修改示例中,网络上的一个节点负责计算栅格线(与上面的图形示例相同)。但是,计算一条线时,应将其数据通过网络发送到另一个节点,该节点将执行显示功能。
这是我们修改后的main()(来自原始示例,没有线程):

int
main (int argc, char **argv) {
    int x1;
    ... // perform initializations
    for (x1 = 0; x1 < num_x_lines; x1++) {
        do_one_line (x1); // "C" in our diagram, below tx_one_line_wait_ack (x1); // "X" and "W" in diagram below
    } 
}

您会注意到我们已经删除了显示部分,而是添加了一个tx一行等待ack()函数。进一步假设我们正在处理一个相当慢的网络,但是CPU并没有真正参与传输方面-它将数据发射到某些硬件上,然后担心传输数据。 tx一行等待ack()使用一点CPU将数据发送到硬件,但是在等待来自远端的确认时不使用CPU。
(略)65

More on synchronization

We’ve already seen:
• mutexes
• semaphores
• barriers
Let’s now finish up our discussion of synchronization by talking about:
• readers/writer locks
• sleepon locks
• condition variables
• additional Neutrino services

Readers/writer locks

读取器和写入器锁用于其名称的确切含义:多个读取器可以使用没有写入器的资源,或者一个写入器可以使用没有其他写入器或读取器的资源。这种情况经常发生,足以保证专门用于此目的的特殊类型的同步原语。通常,您将拥有由一堆线程共享的数据结构。显然,一次只能有一个线程写入数据结构。如果要写入多个线程,则这些线程可能会覆盖彼此的数据。
为了防止这种情况的发生,写入线程将以独占方式获取“ rwlock”(读取器/写入器锁),这意味着它只有访问数据结构的权限。请注意,访问权限的专有性是通过自愿手段严格控制的。系统设计师可以自行决定使用rwlocks来同步所有与数据区域相关的线程。
读者则相反。由于读取数据区域是一种非破坏性的操作,因此可以有任意数量的线程正在读取数据(即使它与另一个线程正在读取的是同一数据)。这里的隐含点是,当任何一个或多个线程正在从数据区域读取数据时,没有线程可以将其写入数据区域。否则,读取线程可能会因读取一部分数据而被写入线程抢占,然后在读取线程恢复时继续读取数据,但从新的数据“更新”中读取数据而感到困惑。
这样会导致数据不一致。让我们看一下您将与rwlocks一起使用的调用。前两个调用用于初始化rwlock的库的内部存储区域:

		int
		pthread_rwlock_init (pthread_rwlock_t *lock,
		const pthread_rwlockattr_t *attr);
		int
		pthread_rwlock_destroy (pthread_rwlock_t *lock);

The pthread_rwlock_init() function takes the lock argument (of type pthread_rwlock_t) and initializes it based on the attributes specified by attr. We’re just going to use an attribute of NULL in our examples, which means, “Use the defaults.” For detailed information about the attributes, see the library reference pages for pthread_rwlockattr_init(), pthread_rwlockattr_destroy(), pthread_rwlockattr_getpshared(), and pthread_rwlockattr_setpshared().
使用rwlock完成操作后,通常会调用pthread rwlock destroy()来销毁该锁,从而使该锁无效。您永远不要使用已被破坏或尚未初始化的锁。
接下来,我们需要获取适当类型的锁。如上所述,基本上有两种锁定模式:读取者将需要“非排他”访问,而写入者将需要“排他”访问。为了保持名称简单,这些函数以锁的用户命名:

		int
		pthread_rwlock_rdlock (pthread_rwlock_t *lock);
		int
		pthread_rwlock_tryrdlock (pthread_rwlock_t *lock);
		int
		pthread_rwlock_wrlock (pthread_rwlock_t *lock);
		int
		pthread_rwlock_trywrlock (pthread_rwlock_t *lock);

(略)74

Sleepon locks

多线程程序中发生的另一种常见情况是需要线程等待“事情发生”。
这个“东西”可以是任何东西!可能是这样的事实:现在可以从设备获得数据,或者传送带现在已经移至正确的位置,或者数据已经提交到磁盘,或者其他任何情况。这里要提出的另一种说法是,多个线程可能需要等待给定的事件。为此,我们将使用条件变量condition variable(我们将在下面看到)或简单得多的“ sleepon”锁。要使用sleepon锁,您实际上需要执行一些操作。让我们先看一下调用,然后看一下如何使用锁。

int
pthread_sleepon_lock (void);
int
pthread_sleepon_unlock (void);
int
pthread_sleepon_broadcast (void *addr);
int
pthread_sleepon_signal (void *addr);
int
pthread_sleepon_wait (void *addr);

如上所述,线程需要等待某些事情发生。上面的函数列表中最明显的选择是pthread sleepon wait()。但是首先,线程需要检查它是否确实必须等待。让我们建立一个例子。一个线程是生产者线程,它从某种硬件上获取数据。另一个线程是使用者线程,它对刚到达的数据进行某种形式的处理。
让我们先看一下消费者:


volatile int data_ready = 0;
consumer () {
	while (1) {
		while (!data_ready) {
			// WAIT 
      	  	// process data
    	}
	}
}

消费者正坐在其主处理循环中(while(1));它将永远做下去。它要做的第一件事是查看数据就绪标志。如果该标志为0,则表示没有数据就绪。因此,消费者应该等待。生产者将以某种方式将其唤醒,此时消费者应重新检查其数据就绪标志, reexamine its data_ready flag。假设这就是发生的情况,并且使用者查看该标志并确定其为1,这意味着现在可以使用数据。使用者离开并处理数据,然后去查看是否还有更多工作要做,依此类推。我们将在这里遇到一个问题。
消费者如何与生产者同步地重置数据就绪标志?
显然,我们将需要某种形式的标志独占访问,以便这些线程中只有一个在给定时间对其进行修改。
在这种情况下使用的方法是用互斥锁构建的,但是它是埋在sleepon库的实现中的互斥锁,因此我们只能通过两个函数访问它:pthread sleepon lock()pthread sleepon unlock()
让我们修改我们的消费者consumer:

consumer () {
	while (1) { 
		pthread_sleepon_lock (); 
		while (!data_ready) {
			// WAIT 
				}
			// process data
			data_ready = 0;
			pthread_sleepon_unlock ();
  } 
}

现在,我们在使用者的操作周围添加了锁定和解锁。这意味着使用者现在可以在没有竞争条件的情况下可靠地测试数据就绪标志data_ready flag,并且还可以可靠地设置该标志。
好,太棒了。现在,“ WAIT”调用如何处理?正如我们之前所建议的,它实际上是pthread sleepon wait()调用。这是第二个while循环:

while (!data_ready) { 
	pthread_sleepon_wait (&data_ready);
}

pthread sleeponwait()实际上执行三个不同的步骤!

  1. 解锁sleepon库互斥体。
  2. 执行等待操作。
  3. 重新锁定sleepon库互斥锁。
    它必须解锁和锁定sleepon库的互斥锁的原因很简单-因为互斥锁的整个思想是确保互斥数据就绪变量,这意味着我们希望锁定生产者,使其在接触数据就绪变量时我们正在测试。但是,如果我们不执行操作的解锁部分,那么生产者将永远无法设置它来告诉我们数据确实可用!重新锁定操作纯粹是为了方便起见;这样的用户pthread sleepon wait()不必在唤醒时担心锁的状态。
    让我们切换到生产者端,看看它如何使用sleepon库。这是完整的实现:
producer () {
    while (1) {
// wait for interrupt from hardware here... 
        pthread_sleepon_lock ();
        data_ready = 1;
        pthread_sleepon_signal (&data_ready);
        pthread_sleepon_unlock ();
    }
}

如您所见,生产者也锁定互斥锁,以便它可以独占访问数据就绪data_ready variable变量以进行设置。让我们详细研究会发生什么。我们将消费者和生产者州确定为:
DraggedImage-18.png
(略)78

Condition variables

条件变量(或“ condvars”)与我们上面刚刚看到的sleepon锁非常相似。实际上,sleepon锁是在condvars之上构建的,这就是为什么我们在sleepon示例的说明表中拥有CONDVAR状态的原因。需要重复一下,pthread cond wait()函数释放互斥锁,等待,然后重新获取该互斥锁,就像pthread sleepon wait()函数一样。让我们跳过预备知识,并使用condvars重做sleepon部分中的生产者和消费者的示例。
然后,我们将讨论通话。


/*
* cp1.c
*/
#include <stdio.h>
#include <pthread.h>
int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;//条件变量
void *
consumer (void *notused)
{
    printf ("In consumer thread...\n");
    while (1) {
        pthread_mutex_lock (&mutex);
        while (!data_ready) {
            pthread_cond_wait (&condvar, &mutex);
        }
        // process data
        printf ("consumer: got data from producer\n");
        data_ready = 0;
        pthread_cond_signal (&condvar);
        pthread_mutex_unlock (&mutex);


    }
}

void *producer (void *notused)
{
    printf ("In producer thread...\n");
    while (1) {
// get data from hardware
// we’ll simulate this with a sleep (1)
        sleep (1);
        printf ("producer: got data from h/w\n");
        pthread_mutex_lock (&mutex);
        while (data_ready) {
            pthread_cond_wait (&condvar, &mutex);
        }
        data_ready = 1;
        pthread_cond_signal (&condvar);
        pthread_mutex_unlock (&mutex);
    }
}
main () {
    printf ("Starting consumer/producer example...\n");
// create the producer and consumer threads
 pthread_create (NULL, NULL, producer, NULL);
 pthread_create (NULL, NULL, consumer, NULL);
// let the threads run for a bit
    sleep (20);
}

与我们刚才看到的sleepon示例几乎完全相同,但有一些变化(我们还添加了一些printf()函数和main()以便程序可以运行!)立即,我们看到的第一件事是新数据类型:pthread cond t。这只是条件变量的声明;我们称其为condvar
我们注意到的下一件事情是,消费者的结构与前面的sleepon示例中的消费者的结构相同。我们已经更换了pthread sleepon lock()和pthread sleepon unlock()与标准互斥锁版本(pthread Mutex Lock()和pthread Mutex unlock()。)pthread sleepon wait()已替换为pthread cond wait()
主要的区别是sleepon库中有一个互斥量埋在其中深处,而当我们使用condvars时,我们显式地传递了互斥量。这样,我们可以获得更多的灵活性。最后,我们注意到我们得到了pthread cond signal()而不是pthread sleepon signal()(同样,显式地传递了互斥锁)。
(略)80

发布了10 篇原创文章 · 获赞 1 · 访问量 522

猜你喜欢

转载自blog.csdn.net/chenshiming1995/article/details/104923466