← 返回首页
读书笔记

《Operating System:Three Easy Pieces》第二十七章 插叙:进程API

线程创建

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

thread

attr

start_routine

arg

例子:

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

c
int pthread_join(pthread_t thread, void **value_ptr); 1 #include <pthread.h> 2 3 typedef struct myarg_t { 4 int a; 5 int b; 6 } myarg_t; 7 8 void *mythread(void *arg) { 9 myarg_t *m = (myarg_t *) arg; 10 printf("%d %d\n", m->a, m->b); 11 return NULL; 12 } 13 14 int 15 main(int argc, char *argv[]) { 16 pthread_t p; 17 int rc; 18 19 myarg_t args; 20 args.a = 10; 21 args.b = 20; 22 rc = pthread_create(&p, NULL, mythread, &args); 23 ... 24 }

线程完成

通过pthread_join阻塞等待线程完成

c
pthread_create(&p, NULL, mythread, (void *) 100); pthread_join(p, (void **) &m);

第一个是 pthread_t 类型,用于指定要等待的线程。这个变量是由线程创建函数初始化的(当你将一个指针作为参数传递给 pthread_create()时)。如果你保留了它,就可以用它来等待该线程终止。 第二个参数是一个指针,指向你希望得到的返回值。因为函数可以返回任何东西,所以它被定义为返回一个指向 void 的指针。因为 pthread_join()函数改变了传入参数的值,所以你需要传入一个指向该值的指针,而不只是该值本身。

c
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <assert.h> 4 #include <stdlib.h> 5 6 typedef struct myarg_t { 7 int a; 8 int b; 9 } myarg_t; 10 11 typedef struct myret_t { 12 int x; 13 int y; 14 } myret_t; 15 16 void *mythread(void *arg) { 17 myarg_t *m = (myarg_t *) arg; 18 printf("%d %d\n", m->a, m->b); 19 myret_t *r = Malloc(sizeof(myret_t)); 20 r->x = 1; 21 r->y = 2; 22 return (void *) r; 23 } 24 25 int 26 main(int argc, char *argv[]) { 27 int rc; 28 pthread_t p; 29 myret_t *m; 30 31 myarg_t args; 32 args.a = 10; 33 args.b = 20; 34 Pthread_create(&p, NULL, mythread, &args); 35 Pthread_join(p, (void **) &m); 36 printf("returned %d %d\n", m->x, m->y); 37 return 0; 38 } // 图 27.2 等待线程完成

其次,如果我们只传入一个值(例如,一个 int),也不必将它打包为一个参数。下边代码展示了一个例子。在这种情况下,更简单一些,因为我们不必在结构中打包参数和返回值。

c
void *mythread(void *arg) { int m = (int) arg; printf("%d\n", m); return (void *) (arg + 1); } int main(int argc, char *argv[]) { pthread_t p; int rc, m; Pthread_create(&p, NULL, mythread, (void *) 100); Pthread_join(p, (void **) &m); printf("returned %d\n", m); return 0; } // 图 27.3 较简单的向线程传递参数示例

除了线程创建和 join 之外,POSIX 线程库提供的最有用的函数集,可能是通过锁(lock)来提供互斥进入临界区的那些函数。这方面最基本的一对函数是:

c
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);

函数应该易于理解和使用。如果你注识到有一段代码是一个临界区,就需要通过锁来保护,以便像需要的那样运行。你大概可以想象代码的样子:

c
pthread_mutex_t lock; pthread_mutex_lock(&lock); x = x + 1; // or whatever your critical section is pthread_mutex_unlock(&lock);

这段代码的注思是:如果在调用 pthread_mutex_lock()时没有其他线程持有锁,线程将获取该锁并进入临界区。如果另一个线程确实持有该锁,那么尝试获取该锁的线程将不会从该调用返回,直到获得该锁(注味着持有该锁的线程通过解锁调用释放该锁)。当然,在给定的时间内,许多线程可能会卡住,在获取锁的函数内部等待。然而,只有获得锁的线程才应该调用解锁。 遗憾的是,这段代码有两个重要的问题。第一个问题是缺乏正确的初始化(lack of proper initialization)。所有锁必须正确初始化,以确保它们具有正确的值,并在锁和解锁被调用时按照需要工作。 对于 POSIX 线程,有两种方法来初始化锁。一种方法是使用 PTHREAD_MUTEX_INITIALIZER,如下所示:

c
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

这样做会将锁设置为默认值,从而使锁可用。初始化的动态方法(即在运行时)是调用 pthread_mutex_init(),如下所示:

c
int rc = pthread_mutex_init(&lock, NULL); assert(rc == 0); // always check success!

此函数的第一个参数是锁本身的地址,而第二个参数是一组可选属性。请你自己去详细了解这些属性。传入 NULL 就是使用默认值。我们通常使用动态(后者)方法。当你用完锁时,还应该相应地调用 pthread_mutex_destroy(),所有细节请参阅手册。 上述代码的第二个问题是在调用获取锁和释放锁时没有检查错误代码。就像 UNIX 系统中调用的任何库函数一样,这些函数也可能会失败!如果你的代码没有正确地检查错误代码,失败将会静静地发生,在这种情况下,可能会允许多个线程进入临界区。至少要使用包注的函数,它对函数成功加上断言(见图 27.4)。更复杂的(非玩具)程序,在出现问题时不能简单地退出,应该检查失败并在获取锁或释放锁未成功时执行适当的操作。

c
// Use this to keep your code clean but check for failures // Only use if exiting program is OK upon failure void Pthread_mutex_lock(pthread_mutex_t *mutex) { int rc = pthread_mutex_lock(mutex); assert(rc == 0); }

获取锁和释放锁函数不是 pthread 与锁进行交互的仅有的函数。特别是,这里有两个你可能感兴趣的函数:

c
int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *abs_timeout);

这两个调用用于获取锁。如果锁已被占用,则 trylock 版本将失败。获取锁的 timedlock定版本会在超时或获取锁后返回,以先发生者为准。因此,具有零超时的 timedlock 退化为trylock 的情况。通常应避免使用这两种版本,但有些情况下,避免卡在(可能无限期的)获取锁的函数中会很有用,我们将在以后的章节中看到(例如,当我们研究死锁时)。

条件变量

所有线程库还有一个主要组件(当然 POSIX 线程也是如此),就是存在一个条件变量(condition variable)。当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某些操作,条件变量就很有用。希望以这种方式进行交互的程序使用两个主要函数:

c
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_signal(pthread_cond_t *cond);

要使用条件变量,必须另外有一个与此条件相关的锁。在调用上述任何一个函数时,应该持有这个锁。 第一个函数 pthread_cond_wait()使调用线程进入休眠状态,因此等待其他线程发出信号,通 常当程序中的某些内容发生变化时,现在正在休眠的线程可能会关心它。典型的用法如下所示:

c
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; Pthread_mutex_lock(&lock); while (ready == 0) Pthread_cond_wait(&cond, &lock); Pthread_mutex_unlock(&lock);

唤醒线程的代码运行在另外某个线程中,调用pthread_cond_signal时也需要持有对应锁

像下面这样:

c
Pthread_mutex_lock(&lock); ready = 1; Pthread_cond_signal(&cond); Pthread_mutex_unlock(&lock);

关于这段代码有一些注意事项:

pthread_cond_wait有第二个参数,因为它会隐式释放锁,以便在其线程休眠后唤醒线程可以获取锁,之后又会重新获得锁。 请注意,有时候线程之间不用条件变量和锁,用一个标记变量会看起来很简单,很吸引人。例如,我们可以重写上面的等待代码,像这样:

c
while (ready == 0) ; // spin

相关的发信号代码看起来像这样:

c
ready = 1;

千万不要这么做。首先,多数情况下性能差(长时间的自旋浪费 CPU)。其次,容易出错。最近的研究显示,线程之间通过标志同步(像上面那样),出错的可能性让人吃惊。在那项研究中,这些不正规的同步方法半数以上都是有问题的。

编译和运行

本章所有代码很容易运行。代码需要包括头文件 pthread.h 才能编译。链接时需要 pthread库,增加-pthread 标记。 例如,要编译一个简单的多线程程序,只需像下面这样做:

c
prompt> gcc -o main main.c -Wall -pthread

只要 main.c 包含 pthreads 头文件,你就已经成功地编译了一个并发程序。像往常一样,它是否能工作完全是另一回事。

小结

补充:线程 API 指导

参考

本文由 GJJ 创作,内容来源于 Notion 数据库,随时可在 Notion 中编辑更新。 本站由 DeepSeek-v4-flash 辅助构建,项目参考 NotionNext

← 返回首页
61
文章
6
标签
3
分类
962
运行天数