本章介绍主要的线程 API,更多细节参考 [B89,B97,B+96,K+96]。
关键问题:如何创建和控制线程?操作系统应提供哪些易用且实用的接口来创建和控制线程?
线程创建
编写多线程程序首先需要创建线程。POSIX 标准提供了 pthread_create:
#include <pthread.h>
int
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);该函数包含 4 个参数:
thread:指向pthread_t结构的指针,用于与线程交互。attr:指定线程属性(如栈大小、优先级)。传入NULL使用默认值。start_routine:函数指针,指向线程运行的函数。该函数接收void *参数并返回void *。arg:传递给start_routine的参数。
使用 void * 允许传递和返回任意类型的数据。
以下示例展示了如何创建线程并传递参数(打包在结构体中):
#include <assert.h>
#include <stdio.h>
#include <pthread.h>
typedef struct {
int a;
int b;
} myarg_t;
void *mythread(void *arg) {
myarg_t *args = (myarg_t *) arg;
printf("%d %d\n", args->a, args->b);
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t p;
myarg_t args = { 10, 20 };
int rc = pthread_create(&p, NULL, mythread, &args);
assert(rc == 0);
(void) pthread_join(p, NULL);
printf("done\n");
return 0;
}线程创建后,拥有独立的调用栈,与现有线程在同一地址空间运行。
线程完成
使用 pthread_join() 等待线程完成。
int pthread_join(pthread_t thread, void **retval);thread:要等待的线程。retval:指向返回值的指针的指针。
示例 1:传递和返回复杂数据
使用结构体传递参数,并返回动态分配的结构体指针。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "common_threads.h"
typedef struct { int a; int b; } myarg_t;
typedef struct { int x; int y; } myret_t;
void *mythread(void *arg) {
myarg_t *args = (myarg_t *) arg;
printf("args %d %d\n", args->a, args->b);
myret_t *rvals = malloc(sizeof(myret_t));
assert(rvals != NULL);
rvals->x = 1; rvals->y = 2;
return (void *) rvals;
}
int main(int argc, char *argv[]) {
pthread_t p;
myret_t *rvals;
myarg_t args = { 10, 20 };
Pthread_create(&p, NULL, mythread, &args);
Pthread_join(p, (void **) &rvals);
printf("returned %d %d\n", rvals->x, rvals->y);
free(rvals);
return 0;
}示例 2:传递简单值
对于简单类型(如 int),可以直接转换,无需打包。
void *mythread(void *arg) {
long long int value = (long long int) arg;
printf("%lld\n", value);
return (void *) (value + 1);
}
int main(int argc, char *argv[]) {
pthread_t p;
long long int rvalue;
Pthread_create(&p, NULL, mythread, (void *) 100);
Pthread_join(p, (void **) &rvalue);
printf("returned %lld\n", rvalue);
return 0;
}警告:不要返回栈指针
永远不要返回指向线程栈上分配变量的指针。线程返回后栈会被释放,导致未定义行为。
void *mythread(void *arg) {
myarg_t *m = (myarg_t *) arg;
printf("%d %d\n", m->a, m->b);
myret_t r; // ALLOCATED ON STACK: BAD!
r.x = 1;
r.y = 2;
return (void *) &r;
} 使用场景
- 并行计算:通常使用
join确保所有任务完成。 - 长期服务(如 Web 服务器):工作线程可能无限期运行,不需要
join。
锁
POSIX 线程库通过互斥锁(mutex)保护临界区。最基本的函数是:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex); 基本用法
在进入临界区前加锁,退出后解锁:
pthread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1; // 临界区
pthread_mutex_unlock(&lock); 如果锁已被占用,调用 pthread_mutex_lock 的线程将阻塞,直到锁被释放。
初始化
锁必须在使用前初始化。
- 静态初始化(使用宏):
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; - 动态初始化(运行时):
- 第二个参数用于设置属性(传入
NULL使用默认值)。 - 使用完毕后,需调用
pthread_mutex_destroy()销毁。
- 第二个参数用于设置属性(传入
int rc = pthread_mutex_init(&lock, NULL);
assert(rc == 0); 错误检查
必须检查锁函数的返回值。对于简单程序,可以使用断言包装器:
void Pthread_mutex_lock(pthread_mutex_t *mutex) {
int rc = pthread_mutex_lock(mutex);
assert(rc == 0);
}其他获取锁的方式
pthread_mutex_trylock:尝试获取锁,如果已被占用则立即失败(不阻塞)。pthread_mutex_timedlock:尝试获取锁,但在超时后返回。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *abs_timeout); 这些函数在避免死锁等场景中很有用。
条件变量
条件变量(condition variable)用于线程间的信号通知,通常与互斥锁配合使用。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond); 标准用法
等待线程:
- 获取锁。
- 检查条件(使用
while循环)。 - 如果条件不满足,调用
wait(自动释放锁并休眠)。 - 被唤醒后(重新获取锁),再次检查条件。
- 条件满足,执行操作,释放锁。
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_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock); 关键点
- 始终持有锁:调用
wait和signal时必须持有锁。 - 使用 while 循环:必须在
while循环中等待,以处理虚假唤醒(spurious wakeups)和状态变更。 - 避免自旋:不要使用简单的标志变量自旋等待(如
while(ready == 0);),这会浪费 CPU 且容易出错 [X+10]。
编译和运行
编译多线程程序需要:
- 包含头文件
#include <pthread.h>。 - 链接时添加
-pthread标志。
prompt> gcc -o main main.c -Wall -pthread 小结
本章介绍了 POSIX 线程库的核心功能:
- 线程创建:
pthread_create - 互斥锁:
pthread_mutex_lock/unlock - 条件变量:
pthread_cond_wait/signal
编写健壮的多线程代码需要耐心和细心。更多 API 细节可参考 man -k pthread。
线程 API 指导构建多线程程序时的关键建议:
- 保持简洁:锁和信号代码应尽可能简单,避免复杂的交互导致缺陷。
- 最小化交互:尽量减少线程间的交互,并使用验证过的方法。
- 初始化:必须初始化锁和条件变量。
- 检查返回值:忽略错误会导致难以调试的行为。
- 注意生命周期:避免传递或返回指向栈上变量的指针。
- 理解栈:每个线程拥有独立的栈。共享数据应存放在堆或全局区域。
- 正确同步:使用条件变量发送信号,禁止使用标记变量自旋。
- 查阅手册:Linux
man手册包含更多细节。