Interlude: Thread API-线程API

本章简要介绍了线程API的主要部分。在接下来的章节中,我们将进一步解释每个部分,说明如何使用API。更多的细节可以在各种书籍和在线资料中找到[B89, B97, B+96, K+96]。我们应该注意到,后面的章节更慢地介绍了锁和条件变量的概念,有很多例子;因此,本章更好地作为参考。

如何创建和控制线程?

操作系统应该为线程创建和控制提供哪些接口?这些接口应该如何设计,以方便使用和实用?

27.1创建线程

要编写多线程程序,首先要做的就是创建新线程,因此必须存在某种线程创建接口。在POSIX中,这很简单。


这个声明可能看起来有点复杂(特别是如果您没有使用C中的函数指针),但实际上它还不错。有四个参数:线程、attr、start例程和arg。

第一个线程是一个指向类型pthread t的结构的指针。我们将使用这个结构来与这个线程交互,因此我们需要将它传递给pthread创建()来初始化它。

第二个参数attr用于指定该线程可能拥有的任何属性。一些例子包括设置堆栈大小,或者关于线程的调度优先级的信息。一个属性是用一个单独的调用pthread attr init()来初始化的;详情请参阅手册页.但是,在大多数情况下,默认值是可以的;在本例中,我们将简单地传递值NULL。

第三个参数是最复杂的,但实际上只是问:这个线程应该从哪个函数开始运行?在C中,我们将此称为函数指针,这告诉我们如下所示:一个函数名(start例程),它通过一个void *类型的单一参数(如在开始例程后的括号中所示),并返回void *类型的值。,一个空指针.如果这个例程需要一个整数参数,而不是一个空指针,那么该声明将是这样的:


最后,第四个参数arg正是要传递给线程开始执行的函数的参数。您可能会问:为什么我们需要这些空指针?答案很简单:用一个空指针作为函数的参数开始例程允许我们传递任何类型的参数;将其作为返回值允许线程返回任何类型的结果.

让我们看一个图27.1中的示例。这里我们创建一个线程。 它传递了两个参数,被打包成一个单独的类型,我们定义自己(myarg t),一旦创建了这个线程,就可以简单地抛出它的参数。 对于它所期望的类型,并按需要解压缩参数。还有啊!一旦您创建了一个线程,您就真正拥有了另一个实时执行实体,它有自己的调用堆栈,在同一个地址空间中运行,就像程序中所有当前存在的线程一样。乐趣从而开始

27.2线程完成

上面的例子演示了如何创建一个线程。但是,如果您希望等待线程完成,会发生什么情况呢?你需要做一些特别的事情来等待完成;特别是,您必须调用常规pthread join()。



这个例程需要两个参数.第一个是pthread t类型,用于指定要等待的线程。这个变量是由线程创建例程初始化的(当您传递一个指向pthread创建的参数的指针时)。如果您保留它,您可以使用它等待线程终止。
第二个参数是指向要返回的返回值的指针。因为例程可以返回任何东西,所以它被定义为返回一个指向void的指针;因为pthread join()例程改变了参数传递的值,所以您需要传递一个指向该值的指针,而不仅仅是值本身。 让我们看另一个例子(图27.2,第4页)。
在代码中,再次创建了一个线程,并通过myarg t结构传递了几个参数。要返回值,将使用myret t类型。一旦线程完成运行,主线程(在pthread join() routine1中等待),然后返回,我们就可以访问从线程返回的值,即myret t中的任何值。
关于这个例子有几点需要注意。首先,我们通常不需要做所有这些痛苦的包装和争论。例如,如果我们只是创建一个没有参数的线程,那么当线程创建时,我们可以将NULL作为参数传递。类似地,如果我们不关心返回值,则可以将NULL传递给pthread,join()。第二,如果我们只是传递一个值(例如,int)。必须把它作为一个论点加以包装。图27.3(第5页)显示了一个示例。在这种情况下,生命要简单一些,因为我们不需要在结构内部打包参数和返回值。
第三,我们应该注意到,对于从线程返回值的方式,必须非常小心。特别是,永远不要返回一个指针,它指的是在线程的调用堆栈上分配的某个东西。如果你这样做了,你认为会发生什么?(想想!)下面是一个危险的代码示例,由图27.3中的示例修改。

在这种情况下,变量r被分配到mythread的堆栈上。然而,当它返回时,值会自动被释放(这就是为什么堆栈如此容易使用,毕竟!),因此,将指针传回到现在的解除配置,变量会导致各种各样的坏结果。当然,当您打印出您认为您返回的值时,您可能(但不一定!)会感到惊讶。试一试,为自己找出答案。
最后,您可能会注意到,使用pthread创建()来创建线程,然后立即调用pthread join(),这是创建线程的一种非常奇怪的方式。事实上,有一个更简单的方法来完成这个任务;这叫做程序调用。显然,我们通常会创建不止一个线程并等待它完成,否则根本就没有使用线程的目的。
   我们应该注意,不是所有的代码都是多线程的,使用join例程。例如,一个多线程的web服务器可能会创建多个工作线程,然后使用主线程来接受请求并将它们无限期地传递给工作人员。这样的长期项目可能不需要加入。然而,一个创建线程来执行特定任务(并行)的并行程序很可能会使用join,以确保所有这些工作在退出或进入下一个计算阶段之前完成。

27.3锁
除了线程创建和连接之外,POSIX threads库提供的下一个最有用的功能集可能是通过锁为关键部分提供互斥的功能下面给出了用于此目的的最基本的例程
例程应该易于理解和使用。当您有一个代码区域是一个关键部分,因此需要保护以确保正确的操作时,锁是非常有用的。您可以想象一下代码的样子。
代码的意图如下:如果在调用pthread互斥锁()时没有其他线程持有锁,那么线程将获得锁并进入关键部分。如果另一个线程确实持有锁,那么试图获取锁的线程将不会从调用返回,直到它获得了锁(这意味着锁的线程已经通过解锁调用释放了它)。当然,在给定的时间内,许多线程可能在锁捕获函数内等待;不过,只有获得了锁的线程才应该调用解锁。 不幸的是,这段代码在两个重要的方面被打破了。第一个 问题是缺少适当的初始化。所有锁必须正确。 初始化,以保证它们有正确的开始值。 当锁和解锁被调用时,使用并因此工作。
   对于POSIX线程,有两种方法来初始化锁。一种方法是使用PTHREAD MUTEX初始化器,如下所示。这样做会将锁设置为默认值,从而使锁可用。动态的方法。在运行时,将调用pthread mutex init(),如下所示。
这个例程的第一个参数是锁本身的地址,而第二个参数是可选的属性集。阅读更多关于你自己的属性;在简单地使用默认值时传递NULL。无论哪种方法都可行,但我们通常使用动态(后一种)方法。注意,当您使用锁完成时,也应该对pthread互斥对象进行相应的调用。参见手册页了解所有细节
上面代码的第二个问题是,当调用lock和unlock时,它不能检查错误代码。就像在UNIX系统中调用的任何一个库程序一样,这些例程也会失败!如果您的代码没有正确地检查错误代码,那么失败将会悄无声息地发生,在这种情况下可以允许多个线程进入一个关键的部分。
最低限度地使用包装器,它断言例程成功了(例如,如图27.4所示);更复杂的(非玩具)程序,当出现错误时不能简单地退出,应该检查失败并在锁定或解锁失败时做一些适当的事情。

锁和解锁例程并不是pthreads库中与锁交互的唯一例程。特别地,这里有两个可能感兴趣的程序。
这两个调用用于锁捕获。如果锁已被保存,trylock版本将返回失败;获取锁的timedlock版本在超时后或获取锁后返回,无论哪一个先发生。因此,timedlock超时为零,就会退化到trylock实例。这两个版本通常都应该避免;然而,在一些情况下,在锁定获取例程中避免被卡住(可能是无限的)可能是有用的,我们将在以后的章节中看到(例如,当我们研究死锁时)。

27.4条件变量
任何线程库的其他主要组件,当然还有POSIX线程的情况,都是条件变量的存在。 条件变量在线程之间必须进行某种信号传递时是有用的,如果一个线程在等待另一个线程在它可以继续之前做某事。 程序使用两个主例程,希望以这种方式进行交互。
要使用条件变量,必须添加一个与此条件相关联的锁。当调用上述例程中的任何一个时,该锁应该被保持。
第一个例程,pthread cond wait(),将调用线程放入休眠状态,因此等待其他线程来发出信号,通常在程序中某些东西发生了变化,而现在睡眠线程可能会关心这个问题。
一个典型的用法是这样的。
在这个代码中,在相关锁和条件的初始化之后,一个线程检查这个变量是否已经被设置成除了0之外的其他东西。如果不是,线程只调用等待程序,直到其他线程唤醒它。唤醒一个线程的代码,它将在其他线程中运行,看起来是这样的。
关于这个代码序列有几点需要注意。首先,当发送信号时(以及在修改全局变量时),我们总是确保持有锁。这确保了我们不会意外地将一个竞态条件引入到代码中其次,您可能会注意到等待调用将锁作为第二个参数,而信号调用只需要一个条件。造成这种差异的原因是,等待调用除了将调用线程放入睡眠之外,还会在调用者睡觉时释放锁。想象一下,如果它没有这样做:其他线程如何获得锁并发出信号以唤醒它?然而,在被唤醒后返回之前,pthread cond wait()会重新获得锁,从而确保在等待序列的开始时,等待线程在锁获取之间运行。
最后一个奇怪的地方:等待线程在while循环中重新检查条件,而不是简单的if语句。当我们在将来的章节中学习条件变量时,我们将详细讨论这个问题,但是一般来说,使用while循环是一件简单而安全的事情。

尽管它会重新检查条件(可能会增加一些开销),但是有一些pthread实现可能会错误地唤醒一个等待线程;在这种情况下,如果没有重新检查,等待的线程会继续认为条件已经发生了变化,尽管它没有改变。因此,把醒来看作是可能发生了变化的暗示,而不是一个绝对的事实,是更安全的
请注意,有时在两个线程之间使用简单的标记来传递信号,而不是使用条件变量和关联的锁。例如,我们可以重写上面的等待代码,以便在等待代码中看起来更像这样。
相关的信号代码如下所示。
不要这样做,理由如下。首先,在许多情况下,它的性能很差(旋转很长时间只是浪费CPU周期)。其次,它容易出错。正如最近的研究显示[X+10],在使用标志(如上)在线程之间进行同步时,很容易犯错误;在该研究中,这些临时同步的使用大约有一半是错误的!不要偷懒;使用条件变量,即使你认为你可以不这样做
如果条件变量听起来很混乱,不要担心太多(然而)我们将在随后的章节中详细地介绍它们。在此之前,我们应该知道它们的存在,以及它们是如何以及为什么被使用的。
27.5编译和运行
本章中的所有代码示例都相对容易启动和运行。要编译它们,必须包括头pthread。在您的代码中。在链接行中,还必须通过添加-pthread标志显式地链接pthreads库。
例如,要编译一个简单的多线程程序,您只需执行以下操作即可。
只要主。c包括pthreads头,您现在已经成功编译了一个并发程序。不管它是否奏效,和往常一样,完全是另一回事。
27.6总结
我们介绍了pthread库的基础知识,包括线程创建、通过锁构建互斥,以及通过条件变量发送信号和等待。除了耐心和大量的关心之外,您不需要编写健壮的、高效的多线程代码。们现在以一组技巧来结束这一章,当您编写多线程代码时,这些技巧可能对您有用(请参阅下面的页面以获取详细信息)。API的其他方面也很有趣;如果您想要更多的信息,请在Linux系统上键入man -k pthread,以查看构成整个接口的100多个api。然而,本文所讨论的基础知识应该能够帮助您构建复杂的(希望,正确和性能)多线程程序。线程的困难部分不是api,而是如何构建并发prog的复杂逻辑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值