线程编程:Pthreads深入解析
立即解锁
发布时间: 2025-08-26 00:03:44 阅读量: 7 订阅数: 27 


并行编程:多核与集群系统的实践指南
### 线程编程:Pthreads 深入解析
在并行计算领域,多线程编程是实现高效计算的重要手段。许多并行计算平台,尤其是多核平台,提供了共享地址空间,基于线程的编程模型自然成为了这些架构的首选。在这个模型中,所有线程都可以访问共享变量。本文将深入探讨 Pthreads 编程,包括线程的创建、管理、同步等方面的内容。
#### 1. Pthreads 概述
Pthreads 即 POSIX 线程模型,它基于 C 语言定义了线程编程的标准。在一个进程中,所有线程共享一个公共的地址空间,这意味着全局变量和动态生成的数据对象可以被该进程的所有线程访问。不过,每个线程都有自己独立的运行时栈,用于控制激活的函数和存储局部变量,这些局部变量只能由执行线程直接访问。
由于线程的运行时栈在线程终止后会被删除,因此将线程 A 运行时栈中的局部变量引用传递给线程 B 是很危险的。
Pthreads 的数据类型、接口定义和宏通常通过头文件 `<pthread.h>` 提供,所以在 Pthreads 程序中必须包含这个头文件。Pthreads 的函数和数据类型遵循一定的命名约定:
- 函数命名形式为 `pthread[ <object>] <operation> ()`,其中 `<operation>` 描述要执行的操作,可选的 `<object>` 描述该操作应用的对象。例如,`pthread_mutex_init()` 用于初始化互斥变量,这里 `<object>` 是 `mutex`,`<operation>` 是 `init`。
- 对于涉及线程操作的函数,会省略 `<object>` 的指定,如 `pthread_create()` 用于创建线程。
- 所有 Pthreads 函数执行成功时返回值为 0,失败时会返回 `<error.h>` 中的错误代码,因此程序中也应包含这个头文件。
- Pthreads 数据类型与 MPI 类似,描述的是不透明对象,其具体实现对程序员隐藏。数据类型命名形式为 `pthread_<object>_t`,例如 `pthread_mutex_t` 表示互斥变量,若省略 `<object>`,则为 `pthread_t` 表示线程。
以下是一些重要的 Pthreads 数据类型及其含义:
| Pthreads 数据类型 | 含义 |
| --- | --- |
| `pthread_t` | 线程 ID |
| `pthread_mutex_t` | 互斥变量 |
| `pthread_cond_t` | 条件变量 |
| `pthread_key_t` | 访问键 |
| `pthread_attr_t` | 线程属性对象 |
| `pthread_mutexattr_t` | 互斥属性对象 |
| `pthread_condattr_t` | 条件变量属性对象 |
| `pthread_once_t` | 一次性初始化控制上下文 |
线程的执行采用两步调度方法。程序员需要将程序划分为合适数量的用户线程,这些用户线程由库调度器映射到系统线程,然后由操作系统的调度器在计算系统的处理器上执行。程序员无法控制操作系统的调度器,对库调度器的影响也很小,因此不能直接将用户级线程映射到计算系统的处理器上。不过,大多数情况下,库和操作系统提供的调度能取得不错的效果,减轻了程序员的编程负担。
#### 2. 创建和合并线程
当一个 Pthreads 程序启动时,会有一个主线程执行 `main()` 函数。主线程可以通过调用 `pthread_create()` 函数创建更多线程:
```c
int pthread_create (pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg)
```
- 第一个参数是指向 `pthread_t` 类型对象的指针,也就是线程标识符(TID),由 `pthread_create()` 生成,后续可用于其他 Pthreads 函数识别该线程。
- 第二个参数是指向 `pthread_attr_t` 类型的属性对象的指针,用于定义生成线程的期望属性。若为 `NULL`,则生成具有默认属性的线程。若需要不同的属性值,需要在调用 `pthread_create()` 之前创建并初始化属性数据结构。
- 第三个参数指定生成线程要执行的函数 `start_routine()`,该函数应接受一个 `void *` 类型的参数,并返回相同类型的值。
- 第四个参数是指向传递给线程函数 `start_routine()` 的参数值的指针。
如果线程函数需要多个参数,必须将所有参数放入一个数据结构中,然后将该数据结构的地址作为参数传递给线程函数。如果父线程使用相同的线程函数启动多个线程,但参数值不同,应为每个线程使用单独的数据结构来指定参数,以避免参数值被过早覆盖或被多个子线程并发修改的问题。
线程可以通过调用 `pthread_self()` 函数确定自己的线程标识符:
```c
pthread_t pthread_self()
```
要比较两个线程的线程 ID,可以使用 `pthread_equal()` 函数:
```c
int pthread_equal (pthread_t t1, pthread_t t2)
```
该函数在 `t1` 和 `t2` 不指向同一个线程时返回 0,否则返回非零值。由于 `pthread_t` 是不透明的数据结构,只能使用 `pthread_equal()` 来比较线程 ID。
一个进程能够创建的线程数量通常受系统限制。Pthreads 标准规定每个进程至少可以创建 64 个线程,但具体系统的限制可能更高。在大多数系统中,可以通过调用 `sysconf (SC_THREAD_THREADS_MAX)` 来确定最大可启动的线程数:
```c
maxThreads = sysconf (SC_THREAD_THREADS_MAX)
```
知道这个限制后,程序可以避免启动超过最大数量的线程。如果达到限制,调用 `pthread_create()` 函数将返回错误值 `EAGAIN`。
线程可以通过以下两种方式终止:
- 线程函数执行完毕,例如调用 `return`。
- 线程显式调用 `pthread_exit()` 函数:
```c
void pthread_exit (void *valuep)
```
参数 `valuep` 指定要返回给等待该线程终止的其他线程的值。当线程函数终止时,会隐式调用 `pthread_exit()`,并将线程函数的返回值作为参数。需要注意的是,线程的返回值不应是指向线程函数或其调用的其他函数的局部变量的指针,因为这些局部变量存储在运行时栈上,线程终止后可能不再存在,其内存空间可能会被其他线程重用。建议使用全局变量或动态分配的变量。
一个线程可以通过调用 `pthread_join()` 函数等待另一个线程终止:
```c
int pthread_join (pthread_t thread, void **valuep)
```
- 参数 `thread` 指定要等待终止的线程的 TID。
- 参数 `valuep` 指定存储该线程返回值的内存地址。
调用 `pthread_join()` 的线程会被阻塞,直到指定的线程终止。如果多个线程使用 `pthread_join()` 等待同一个线程终止,所有等待线程都会被阻塞,直到该线程终止,但只有一个等待线程能成功存储返回值,其他等待线程调用 `pthread_join()` 的返回值为错误值 `ESRCH`。
Pthreads 库的运行时系统为每个线程分配一个内部数据结构,用于存储控制线程执行所需的信息和数据。在调用 `pthread_join()` 后,终止线程的内部数据结构会被释放,无法再访问。如果没有对某个线程调用 `pthread_join()`,该线程终止后其内部数据结构不会被释放,会一直占用内存空间,直到整个进程终止。为避免这种情况,可以调用 `pthread_detach()` 函数:
```c
int pthread_detach (pthread_t thread)
```
该函数通知运行时系统,具有 TID `thread` 的线程终止后,其内部数据结构可以立即分离。线程可以自行分离,也可以由其他线程分离。线程进入分离状态后,调用 `pthread_join()` 会返回错误值 `EINVAL`。
下面是一个 Pthreads 程序的示例,用于两个矩阵的乘法:
```c
#include <pthread.h>
typedef struct {
int size, row, column;
double (*MA)[8], (*MB)[8], (*MC)[8];
} matrix_type_t;
void *thread_mult (void *w) {
matrix_type_t *work = (matrix_type_t *) w;
int i, row = work->row, column = work->column;
work -> MC[row][column] = 0;
for (i=0; i < work->size; i++)
work->MC[row][column] += work->MA[row][i] * work->MB[i][column];
return NULL;
}
int main() {
int row, column, size = 8, i;
double MA[8][8], MB[8][8], MC[8][8];
matrix_type_t *work;
pthread_t thread[8*8];
for (row=0; row<size; row++)
for (column=0; column<size; column++) {
work = (matrix_type_t *) malloc (sizeof (matrix_type_t));
work->size = size;
work->row = row;
work->column = column;
work->MA = MA; work->MB = MB; work->MC = MC;
pthread_create (&(thread[column + row*8]), NULL,
thread_mult, (void *) work);
}
for (i=0; i<size*size; i++)
pthread_join (thread[i], NULL);
}
```
在这个示例中,为结果矩阵 `MC` 的每个元素创建一个单独的线程。每个线程执行 `thread_mult()` 函数,计算输入矩阵 `MA` 的一行和 `MB` 的一列的标量积。主线程创建所有线程后,使用 `pthread_join()` 等待它们终止。不过,这个程序的可扩展性较差,因为为输出矩阵的每个元素创建一个线程,即使对于中等大小的矩阵,也可能达到系统允许的最大线程数。对于更大的矩阵,应该使用固定数量的线程,每个线程计算输出矩阵的一个块。
#### 3. Pthreads 线程协调
由于进程中的线程共享公共地址空间,它们可以并发访问共享变量。为避免竞态条件,需要对这些并发访问进行协调。Pthreads 提供了互斥变量和条件变量来实现这种协调。
##### 3.1 互斥变量
在 Pthreads 中,互斥变量是预定义的不透明类型 `pthread_mutex_t` 的数据结构,用于确保对公共数据的互斥访问,即同一时间只有一个线程可以独占访问公共数据结构,其他线程必须等待。
互斥变量有两种状态:锁定和解锁。为确保对公共数据结构的互斥访问,需要为该数据结构分配一个单独的互斥变量。所有访问线程必须遵循以下行为:
- 在访问公共数据结构之前,调用特定的 Pthreads 函数锁定相应的互斥变量。成功锁定后,该线程成为互斥变量的所有者。
- 每次访问公共数据结构后,解锁相应的互斥变量。解锁后,该线程不再是互斥变量的所有者,其他线程可以成为所有者并访问数据结构。
如果线程 A 试图锁定已经被线程 B 拥有的互斥变量,线程 A 会被阻塞,直到线程 B 解锁该互斥变量。Pthreads 运行时系统确保同一时间只有一个线程是特定互斥变量的所有者。但如果线程在访问数据结构之前没有锁定互斥变量,就无法保证互斥访问。
互斥变量与数据结构的分配是由程序员通过使用特定互斥变量的锁定和解锁操作来隐式完成的,没有显式的分配。为了提高 Pthreads 程序的可读性,程序员可以将公共数据结构和保护它的互斥变量组合成一个新的结构。
互斥变量可以静态声明或动态生成,但在使用之前必须初始化:
- 对于静态分配的互斥变量 `mutex`,可以使用预定义的宏 `PTHREAD_MUTEX_INITIALIZER` 进行初始化:
```c
mutex = PTHREAD_MUTEX_INITIALIZER
```
- 对于任意互斥变量(静态分配或动态生成),可以通过调用 `pthread_mutex_init()
0
0
复制全文