第 27 章 插叙:线程 API

本章介绍主要的线程 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 个参数:

  1. thread:指向 pthread_t 结构的指针,用于与线程交互。
  2. attr:指定线程属性(如栈大小、优先级)。传入 NULL 使用默认值。
  3. start_routine:函数指针,指向线程运行的函数。该函数接收 void * 参数并返回 void *
  4. 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 的线程将阻塞,直到锁被释放。

初始化

锁必须在使用前初始化。

  1. 静态初始化(使用宏):
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 
  1. 动态初始化(运行时):
    • 第二个参数用于设置属性(传入 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); 

标准用法

等待线程

  1. 获取锁。
  2. 检查条件(使用 while 循环)。
  3. 如果条件不满足,调用 wait(自动释放锁并休眠)。
  4. 被唤醒后(重新获取锁),再次检查条件。
  5. 条件满足,执行操作,释放锁。
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); 

发送信号线程

  1. 获取锁。
  2. 改变条件。
  3. 发送信号。
  4. 释放锁。
Pthread_mutex_lock(&lock); 
ready = 1; 
Pthread_cond_signal(&cond); 
Pthread_mutex_unlock(&lock); 

关键点

  1. 始终持有锁:调用 waitsignal 时必须持有锁。
  2. 使用 while 循环:必须在 while 循环中等待,以处理虚假唤醒(spurious wakeups)和状态变更。
  3. 避免自旋:不要使用简单的标志变量自旋等待(如 while(ready == 0);),这会浪费 CPU 且容易出错 [X+10]。

编译和运行

编译多线程程序需要:

  1. 包含头文件 #include <pthread.h>
  2. 链接时添加 -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 手册包含更多细节。