作者:Allen B. Downey
原文:Chapter 9 Threads
译者:飞龙
协议:CC BY-NC-SA 4.0
当我在2.3节提到线程的时候,我说过线程就是一种进程。现在我会更仔细地解释它。
当你创建进程时,操作系统会创建一块新的地址空间,它包含text段、static段、和堆区。它也会创建新的“执行线程”,这包括程序计数器和其它硬件状态,以及运行时栈。
text
static
我们目前为止看到的进程都是“单线程”的,也就是说每个地址空间中只运行一个执行线程。在这一章中,你会了解“多线程”的进程,它在相同地址空间内拥有多个运行中的线程。
在单一进程中,所有线程都共享相同的text段,所以它们运行相同的代码。但是不同线程通常运行代码的不同部分。
而且,它们共享相同的static段,所以如果一个线程修改了某个全局变量,其它线程会看到改动。它们也共享堆区,所以线程可以共享动态分配的内存块。
但是每个线程都有它自己的栈。所以线程可以调用函数而不相互影响。通常,线程并不能访问其它线程的局部变量。
这一章的示例代码在本书的仓库中,在名为counter的目录中。有关代码下载的更多信息,请见第零章。
counter
C语言使用的所普遍的线程标准就是POSIX线程,简写为pthread。POSIX标准定义了线程模型和用于创建和控制线程的接口。多数UNIX的版本提供了POSIX的实现。
pthread
译者注:C11标准也提供了POSIX线程的实现。为了避免冲突,函数的前缀改为了thrd。
thrd
使用pthread就像使用大多数C标准库那样:
例如,我包含了下列头文件:
#include <stdio.h> #include <stdlib.h> #include <pthread.h>
前两个是标准库,第三个就是pthread。为了在gcc中和pthread一起编译,你可以在命令行中使用-l选项:
gcc
-l
gcc -g -O2 -o array array.c -lpthread
这会编译名为array.c的源文件,带有调试信息和优化,并链接pthread库,之后生成名为array的可执行文件。
array.c
array
用于创建线程的pthread函数叫做pthread_create。下面的函数展示了如何使用它:
pthread_create
pthread_t make_thread(void *(*entry)(void *), Shared *shared) { int n; pthread_t thread; n = pthread_create(&thread, NULL, entry, (void *)shared); if (n != 0) { perror("pthread_create failed"); exit(-1); } return thread; }
make_thread是一个包装,我编写它便于使pthread_create更加易用,并提供错误检查。
make_thread
pthread_create的返回类型是pthread_t,你可以将其看做新线程的ID或者“句柄”。
pthread_t
如果pthread_create成功了,它会返回0,make_pthread也会返回新线程的句柄。如果出现了错误,pthread_create会返回错误代码,make_thread会打印错误消息并退出。
make_pthread
pthread_create的参数需要一些解释。从第二个开始,Shared是我定义的结构体,用于包含在两个线程之间共享的值。下面的typedef语句创建了这个新类型:
Shared
typedef
typedef struct { int counter; } Shared;
这里,唯一的共享变量是counter,make_shared为Shared结构体分配空间,并且初始化其内容:
make_shared
Shared *make_shared() { int i; Shared *shared = check_malloc(sizeof (Shared)); shared->counter = 0; return shared; }
entry的参数声明为void指针,但在这个程序中我们知道它是一个指向Shared结构体的指针,所以我们可以对其做相应转换,之后将它传给执行实际工作的child_code。
entry
void
child_code
作为一个简单的示例,child_code打印了共享计数器的值,并增加它。
void child_code(Shared *shared) { printf("counter = %d\n", shared->counter); shared->counter++; }
当child_code返回时,entry调用了pthread_exit,它可以用于将一个值传递给回收(join)当前线程的线程。这里,子线程没有什么要返回的,所以我们传递了NULL。
pthread_exit
NULL
最后,下面是创建子线程的代码:
int i; pthread_t child[NUM_CHILDREN]; Shared *shared = make_shared(1000000); for (i=0; i<NUM_CHILDREN; i++) { child[i] = make_thread(entry, shared); }
NUM_CHILDREN是用于定义子线程数量的编译期常量。child是线程句柄的数组。
NUM_CHILDREN
child
当一个线程希望等待其它线程执行完毕,它需要调用pthread_join。下面是我对pthread_join的包装:
pthread_join
void join_thread(pthread_t thread) { int ret = pthread_join(thread, NULL); if (ret == -1) { perror("pthread_join failed"); exit(-1); } }
参数是你想要等待的线程句柄。这个包装所做的事情就是调用pthread_join之后检查结果。
任何线程都可以回收其它线程,但是多数普遍的情况下,父线程创建并回收所有子线程。我们继续使用上一节的例子,下面是等待子线程的代码:
for (i=0; i<NUM_CHILDREN; i++) { join_thread(child[i]); }
这个循环一次等待一个子线程,以它们创建的顺序。没有办法来保证子线程按照顺序执行完毕,但是这个循环在它们不这样的时候也会正确执行。如果某个子线程迟于其它线程,这个循环会等待它,其它子线程也会在同时执行完毕。但是无论如何,所有子线程执行完毕后,循环才会退出。
如果你下载这本书的仓库,你可以在counter/counter.c中找到它。你可以像这样编译并运行它:
counter/counter.c
$ make counter gcc -Wall counter.c -o counter -lpthread $ ./counter
当我以5个子线程运行它时,我获得了如下输出:
counter = 0 counter = 0 counter = 1 counter = 0 counter = 3
当你运行它时,你可能得到了不同的结果。并且如果你再次运行它,你可能每次都得到不同的结果。到底发生了什么呢?
上一个程序的问题就是,子线程访问了共享变量counter,不带任何同步机制,所以在任何线程增加counter之前,这些线程读取到了它的相同值。
下面是一个事件序列,这可以解释上一节的输出:
Child A reads 0 Child B reads 0 Child C reads 0 Child A prints 0 Child B prints 0 Child A sets counter=1 Child D reads 1 Child D prints 1 Child C prints 0 Child A sets counter=1 Child B sets counter=2 Child C sets counter=3 Child E reads 3 Child E prints 3 Child D sets counter=4 Child E sets counter=5
每次你运行这个程序的时候,线程都会在不同时间点上中断,或者调度器可能选择不同的线程来运行,所以时间序列和结果都是不同的。
假设我们需要强行规定一个顺序。例如,我们想让每个线程读到counter的不同值并增加它,让counter的值反映出执行child_code的线程数量。
为了达到这一要求,我们可以使用“互斥体”(mutex),它提供了互斥体对象,来保证一段代码是“互斥”的,也就是说,一次只有一个线程可以执行这段代码。
我编写了一个叫做mutex.c的小型模块,来提供互斥体对象。我会首先向你展示如何使用,之后再展示工作原理。
mutex.c
下面是child_code使用互斥体同步线程的版本:
void child_code(Shared *shared) { mutex_lock(shared->mutex); printf("counter = %d\n", shared->counter); shared->counter++; mutex_unlock(shared->mutex); }
在任何线程访问counter之前,它们需要“锁住”互斥体,这样可以阻塞住所有其它线程。假设线程A锁住互斥体,并且执行到child_code的中间位置。如果线程B到达并执行了mutex,它会被阻塞。
mutex
当线程A执行完毕后,它执行了mutex_unlock,它允许线程B继续执行。实际上,一次只有一个排队中的线程会执行child_code,所以它们不会互相影响。当我以5个子线程运行这段代码时,我会得到:
mutex_unlock
counter = 0 counter = 1 counter = 2 counter = 3 counter = 4
这样就满足了要求。为了使这个方案能够工作,我向Shared结构体中添加了Mutex:
Mutex
typedef struct { int counter; Mutex *mutex; } Shared;
之后在make_shared中初始化它:
Shared *make_shared(int end) { Shared *shared = check_malloc(sizeof(Shared)); shared->counter = 0; shared->mutex = make_mutex(); //-- this line is new return shared; }
这一节的代码在counter_mutex.c中,Mutex的定义在mutex.c中,我会在下一节解释它。
counter_mutex.c
我的Mutex的定义是pthread_mutex_t类型的包装,它定义在POSIX线程API中。
pthread_mutex_t
为了创建POSIX互斥体,你需要为pthread_mutex_t分配空间,之后调用pthread_mutex_init。
pthread_mutex_init
一个问题就是在这个API下,pthread_mutex_t表现为结构体,所以如果你将它作为参数传递,它会复制,这会使互斥体表现不正常。你需要传递pthread_mutex_t的地址来避免这种情况。
我的代码更加容易正确使用。它定义了一个类型,Mutex,它是pthread_mutex_t的更加可读的名称:
#include <pthread.h> typedef pthread_mutex_t Mutex;
之后它定义了make_mutex,它为mutex分配空间并初始化:
make_mutex
Mutex *make_mutex() { Mutex *mutex = check_malloc(sizeof(Mutex)); int n = pthread_mutex_init(mutex, NULL); if (n != 0) perror_exit("make_lock failed"); return mutex; }
返回值是一个指针,你可以将其作为参数传递,而不会有非预期的复制。
对互斥体加锁和解锁的函数都是POSIX函数的简单包装:
void mutex_lock(Mutex *mutex) { int n = pthread_mutex_lock(mutex); if (n != 0) perror_exit("lock failed"); } void mutex_unlock(Mutex *mutex) { int n = pthread_mutex_unlock(mutex); if (n != 0) perror_exit("unlock failed"); }
代码在mutex.c和头文件mutex.h中。
mutex.h
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8