读书笔记
🎢《Operating System:Three Easy Pieces》第二十七章 插叙:进程API
00 分钟
2023-1-6
2023-4-15
type
status
date
slug
summary
tags
category
icon
password
Property
Apr 15, 2023 03:23 PM

线程创建

thread
指向 pthread_t 结构类型的指针,我们将利用这个结构与该线程交互,因此需要将它传入 pthread_create(),以便将它初始化。相当于该线程的身份证
attr
指定该线程可能具有的任何属性。包括设置栈大小,或关于该线程调度优先级的信息等
start_routine
一个函数指针(function pointer),指向要运行的函数
arg
要运行的函数的参数

例子:

这里我们只是创建了一个线程,传入两个参数,它们被打包成一个我们自己定义的类型(myarg_t)。该线程一旦创建,可以简单地将其参数转换为它所期望的类型,从而根据需要将参数解包。

线程完成

通过pthread_join阻塞等待线程完成
第一个是 pthread_t 类型,用于指定要等待的线程。这个变量是由线程创建函数初始化的(当你将一个指针作为参数传递给 pthread_create()时)。如果你保留了它,就可以用它来等待该线程终止。 第二个参数是一个指针,指向你希望得到的返回值。因为函数可以返回任何东西,所以它被定义为返回一个指向 void 的指针。因为 pthread_join()函数改变了传入参数的值,所以你需要传入一个指向该值的指针,而不只是该值本身。
其次,如果我们只传入一个值(例如,一个 int),也不必将它打包为一个参数。下边代码展示了一个例子。在这种情况下,更简单一些,因为我们不必在结构中打包参数和返回值。

除了线程创建和 join 之外,POSIX 线程库提供的最有用的函数集,可能是通过锁(lock)来提供互斥进入临界区的那些函数。这方面最基本的一对函数是:
函数应该易于理解和使用。如果你注识到有一段代码是一个临界区,就需要通过锁来保护,以便像需要的那样运行。你大概可以想象代码的样子:
这段代码的注思是:如果在调用 pthread_mutex_lock()时没有其他线程持有锁,线程将获取该锁并进入临界区。如果另一个线程确实持有该锁,那么尝试获取该锁的线程将不会从该调用返回,直到获得该锁(注味着持有该锁的线程通过解锁调用释放该锁)。当然,在给定的时间内,许多线程可能会卡住,在获取锁的函数内部等待。然而,只有获得锁的线程才应该调用解锁。 遗憾的是,这段代码有两个重要的问题。第一个问题是缺乏正确的初始化(lack of proper initialization)。所有锁必须正确初始化,以确保它们具有正确的值,并在锁和解锁被调用时按照需要工作。 对于 POSIX 线程,有两种方法来初始化锁。一种方法是使用 PTHREAD_MUTEX_INITIALIZER,如下所示:
这样做会将锁设置为默认值,从而使锁可用。初始化的动态方法(即在运行时)是调用 pthread_mutex_init(),如下所示:
此函数的第一个参数是锁本身的地址,而第二个参数是一组可选属性。请你自己去详细了解这些属性。传入 NULL 就是使用默认值。我们通常使用动态(后者)方法。当你用完锁时,还应该相应地调用 pthread_mutex_destroy(),所有细节请参阅手册。 上述代码的第二个问题是在调用获取锁和释放锁时没有检查错误代码。就像 UNIX 系统中调用的任何库函数一样,这些函数也可能会失败!如果你的代码没有正确地检查错误代码,失败将会静静地发生,在这种情况下,可能会允许多个线程进入临界区。至少要使用包注的函数,它对函数成功加上断言(见图 27.4)。更复杂的(非玩具)程序,在出现问题时不能简单地退出,应该检查失败并在获取锁或释放锁未成功时执行适当的操作。
获取锁和释放锁函数不是 pthread 与锁进行交互的仅有的函数。特别是,这里有两个你可能感兴趣的函数:
这两个调用用于获取锁。如果锁已被占用,则 trylock 版本将失败。获取锁的 timedlock定版本会在超时或获取锁后返回,以先发生者为准。因此,具有零超时的 timedlock 退化为trylock 的情况。通常应避免使用这两种版本,但有些情况下,避免卡在(可能无限期的)获取锁的函数中会很有用,我们将在以后的章节中看到(例如,当我们研究死锁时)。

条件变量

所有线程库还有一个主要组件(当然 POSIX 线程也是如此),就是存在一个条件变量(condition variable)。当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某些操作,条件变量就很有用。希望以这种方式进行交互的程序使用两个主要函数:
要使用条件变量,必须另外有一个与此条件相关的锁。在调用上述任何一个函数时,应该持有这个锁。 第一个函数 pthread_cond_wait()使调用线程进入休眠状态,因此等待其他线程发出信号,通 常当程序中的某些内容发生变化时,现在正在休眠的线程可能会关心它。典型的用法如下所示:
唤醒线程的代码运行在另外某个线程中,调用pthread_cond_signal时也需要持有对应锁
像下面这样:
关于这段代码有一些注意事项:
  • 首先,在发出信号时(以及修改全局变量 ready 时),我们始终确保持有锁。这确保我们不会在代码中注外引入竞态条件。
  • 其次,你可能会注注到等待调用将锁作为其第二个参数,而信号调用仅需要一个条件。
pthread_cond_wait有第二个参数,因为它会隐式释放锁,以便在其线程休眠后唤醒线程可以获取锁,之后又会重新获得锁。 请注意,有时候线程之间不用条件变量和锁,用一个标记变量会看起来很简单,很吸引人。例如,我们可以重写上面的等待代码,像这样:
相关的发信号代码看起来像这样:
千万不要这么做。首先,多数情况下性能差(长时间的自旋浪费 CPU)。其次,容易出错。最近的研究显示,线程之间通过标志同步(像上面那样),出错的可能性让人吃惊。在那项研究中,这些不正规的同步方法半数以上都是有问题的。

编译和运行

本章所有代码很容易运行。代码需要包括头文件 pthread.h 才能编译。链接时需要 pthread库,增加-pthread 标记。 例如,要编译一个简单的多线程程序,只需像下面这样做:
只要 main.c 包含 pthreads 头文件,你就已经成功地编译了一个并发程序。像往常一样,它是否能工作完全是另一回事。

小结

补充:线程 API 指导
当你使用 POSIX 线程库(或者实际上,任何线程库)来构建多线程程序时,需要记住一些小而重 要的事情:
  • 保持简洁。最重要的一点,线程之间的锁和信号的代码应该尽可能简洁。复杂的线程交互容易产生缺陷。
  • 让线程交互减到最少。尽量减少线程之间的交互。每次交互都应该想清楚,并用验证过的、正确的方法来实现(很多方法会在后续章节中学习)。
  • 初始化锁和条件变量。未初始化的代码有时工作正常,有时失败,会产生奇怪的结果。
  • 检查返回值。当然,任何 C 和 UNIX 的程序,都应该检查返回值,这里也是一样。否则会导致古怪而难以理解的行为,让你尖叫,或者痛苦地揪自己的头发。
  • 注意传给线程的参数和返回值。具体来说,如果传递在栈上分配的变量的引用,可能就是在犯错误。
  • 每个线程都有自己的栈。类似于上一条,记住每一个线程都有自己的栈。因此,线程局部变量应该是线程私有的,其他线程不应该访问。线程之间共享数据,值要在堆(heap)或者其他全局可访问的位置。
  • 线程之间总是通过条件变量发送信号。切记不要用标记变量来同步。
  • 多查手册。尤其是 Linux 的 pthread 手册,有更多的细节、更丰富的内容。请仔细阅读!

参考

上一篇
《Operating System:Three Easy Pieces》第二十八章 锁
下一篇
《Operating System:Three Easy Pieces》第二十六章 并发:介绍

评论
Loading...