Linux C线程编程全指南

Linux C语言线程编程详解

一、POSIX 线程基础

1.1 什么是线程?

  • 线程(Thread)是进程内的执行单元,是CPU调度的基本单位

  • 同一进程内的多个线程共享:

    • 进程代码段
    • 数据段(全局变量)
    • 打开的文件描述符
    • 信号处理器
    • 当前工作目录
    • 用户ID和组ID
  • 每个线程独有:

    • 线程ID
    • 寄存器状态(包括程序计数器和栈指针)
    • 栈空间
    • errno变量
    • 信号屏蔽字
    • 优先级

1.2 编译链接

使用pthread库需要链接 -lpthread:

bash 复制代码
gcc -o program program.c -lpthread

头文件:

c 复制代码
#include <pthread.h>

二、核心函数详解

2.1 pthread_create() - 创建线程

c 复制代码
int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *), 
                   void *arg);

参数详解:

  1. pthread_t *thread

    • 指向线程标识符的指针
    • 函数成功返回后,新线程的ID会被写入该指针指向的内存
    • pthread_t 是一个不透明类型,不要直接操作其内容
  2. const pthread_attr_t *attr

    • 线程属性对象指针
    • 传入 NULL 使用默认属性
    • 可通过 pthread_attr_init() 初始化后设置:
      • 分离状态(detached/joinable)
      • 栈大小
      • 调度策略
      • 调度优先级
  3. void *(*start_routine)(void *)

    • 线程启动函数指针
    • 函数签名必须是: void* function_name(void* arg)
    • 线程从这个函数开始执行
    • 函数返回时线程终止
  4. void *arg

    • 传递给启动函数的参数
    • 可以传递任何类型的指针(需要类型转换)
    • 传递多个参数时通常使用结构体

返回值:

  • 成功返回 0
  • 失败返回错误码(不设置errno):
    • EAGAIN: 系统资源不足
    • EINVAL: 无效的属性设置
    • EPERM: 无权限设置调度策略

示例:

c 复制代码
void *thread_func(void *arg) {
    int id = *(int *)arg;
    printf("Thread %d running\n", id);
    return NULL;
}

int main() {
    pthread_t tid;
    int thread_id = 1;
    
    if (pthread_create(&tid, NULL, thread_func, &thread_id) != 0) {
        perror("pthread_create failed");
        return 1;
    }
    
    pthread_join(tid, NULL);
    return 0;
}

2.2 pthread_join() - 等待线程结束

c 复制代码
int pthread_join(pthread_t thread, void **retval);

参数详解:

  1. pthread_t thread

    • 要等待的线程ID
    • 必须是joinable状态的线程
    • 不能重复join同一个线程
  2. void **retval

    • 指向指针的指针,用于接收线程返回值
    • 传入 NULL 表示不关心返回值
    • 线程通过 returnpthread_exit() 返回值

返回值:

  • 成功返回 0
  • 失败返回错误码:
    • EDEADLK: 检测到死锁
    • EINVAL: 线程不是joinable的
    • ESRCH: 找不到该线程

示例:

c 复制代码
void *thread_func(void *arg) {
    int *result = malloc(sizeof(int));
    *result = 42;
    return result;
}

int main() {
    pthread_t tid;
    void *ret_val;
    
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_join(tid, &ret_val);
    
    printf("Thread returned: %d\n", *(int *)ret_val);
    free(ret_val);
    return 0;
}

2.3 pthread_exit() - 线程退出

c 复制代码
void pthread_exit(void *retval);

参数详解:

  1. void *retval
    • 线程退出状态,可被 pthread_join() 获取
    • 不要返回指向线程栈上的局部变量的指针
    • 传入 NULL 表示无返回值

注意事项:

  • 如果主线程调用 pthread_exit(),其他线程会继续运行
  • 如果主线程从 main() return,整个进程结束

2.4 pthread_self() - 获取当前线程ID

c 复制代码
pthread_t pthread_self(void);

返回调用线程的ID,总是成功。


2.5 pthread_detach() - 分离线程

c 复制代码
int pthread_detach(pthread_t thread);

参数详解:

  1. pthread_t thread
    • 要分离的线程ID
    • 可以是自己 pthread_detach(pthread_self())

作用:

  • 将线程标记为detached状态
  • 线程结束后资源自动释放,无需join
  • detached的线程不能再被join

2.6 pthread_cancel() - 取消线程

c 复制代码
int pthread_cancel(pthread_t thread);

向指定线程发送取消请求,但不会立即终止线程。线程需要到达取消点才会响应。


三、线程同步

3.1 互斥锁 (Mutex)

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 初始化
int pthread_mutex_init(pthread_mutex_t *mutex, 
                       const pthread_mutexattr_t *attr);

// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 尝试加锁(非阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.2 条件变量 (Condition Variable)

c 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 初始化
int pthread_cond_init(pthread_cond_t *cond, 
                      const pthread_condattr_t *attr);

// 等待条件
int pthread_cond_wait(pthread_cond_t *cond, 
                      pthread_mutex_t *mutex);

// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);

四、常见陷阱和注意事项

4.1 ⚠️ 传递参数的陷阱

错误示例:

c 复制代码
// 危险!循环变量的地址
for (int i = 0; i < 5; i++) {
    pthread_create(&tid[i], NULL, thread_func, &i);  // ❌
}

问题: 所有线程可能读取到相同的值,因为 i 的地址是同一个。

正确做法:

c 复制代码
// 方法1: 为每个线程分配独立内存
for (int i = 0; i < 5; i++) {
    int *arg = malloc(sizeof(int));
    *arg = i;
    pthread_create(&tid[i], NULL, thread_func, arg);
}

// 方法2: 使用数组
int args[5];
for (int i = 0; i < 5; i++) {
    args[i] = i;
    pthread_create(&tid[i], NULL, thread_func, &args[i]);
}

// 方法3: 直接传值(强制类型转换,仅适用于小整数)
for (int i = 0; i < 5; i++) {
    pthread_create(&tid[i], NULL, thread_func, (void *)(intptr_t)i);
}

4.2 ⚠️ 返回栈上的指针

错误示例:

c 复制代码
void *thread_func(void *arg) {
    int result = 42;
    return &result;  // ❌ 返回局部变量地址
}

正确做法:

c 复制代码
// 方法1: 动态分配内存
void *thread_func(void *arg) {
    int *result = malloc(sizeof(int));
    *result = 42;
    return result;  // 调用者负责释放
}

// 方法2: 使用全局变量或静态变量
void *thread_func(void *arg) {
    static int result = 42;
    return &result;
}

4.3 ⚠️ 忘记Join或Detach

问题: 如果线程是joinable状态但从未被join,会导致资源泄漏。

正确做法:

c 复制代码
// 要么join
pthread_join(tid, NULL);

// 要么detach
pthread_detach(tid);

// 或者创建时就设置为detached
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);

4.4 ⚠️ 主线程过早退出

错误示例:

c 复制代码
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    return 0;  // ❌ 主线程退出,整个进程结束
}

正确做法:

c 复制代码
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_join(tid, NULL);  // 等待线程结束
    return 0;
}

// 或者主线程调用pthread_exit
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_exit(NULL);  // 主线程退出,但进程继续
}

4.5 ⚠️ 竞态条件 (Race Condition)

错误示例:

c 复制代码
int counter = 0;

void *thread_func(void *arg) {
    for (int i = 0; i < 100000; i++) {
        counter++;  // ❌ 非原子操作
    }
    return NULL;
}

正确做法:

c 复制代码
int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *thread_func(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

4.6 ⚠️ 死锁 (Deadlock)

常见死锁场景:

  1. 重复加锁
c 复制代码
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);  // ❌ 死锁
  1. 循环等待
c 复制代码
// 线程1
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);

// 线程2
pthread_mutex_lock(&mutex2);  // 可能死锁
pthread_mutex_lock(&mutex1);

避免死锁:

  • 按固定顺序获取锁
  • 使用 pthread_mutex_trylock()
  • 设置超时
  • 避免嵌套锁

4.7 ⚠️ 条件变量的虚假唤醒

错误示例:

c 复制代码
pthread_cond_wait(&cond, &mutex);
// 假设条件满足

正确做法:

c 复制代码
while (!condition_satisfied) {
    pthread_cond_wait(&cond, &mutex);
}
// 现在条件确实满足

使用 while 而不是 if 来检查条件。


4.8 ⚠️ 编译链接错误

常见错误:

bash 复制代码
undefined reference to `pthread_create`

解决方法:

bash 复制代码
gcc program.c -o program -lpthread  # 注意 -lpthread 必须在最后

五、实用技巧

5.1 获取CPU核心数

c 复制代码
#include <unistd.h>

int num_cores = sysconf(_SC_NPROCESSORS_ONLN);

5.2 线程数组管理

c 复制代码
#define NUM_THREADS 4

pthread_t threads[NUM_THREADS];

// 创建
for (int i = 0; i < NUM_THREADS; i++) {
    pthread_create(&threads[i], NULL, thread_func, &args[i]);
}

// 等待
for (int i = 0; i < NUM_THREADS; i++) {
    pthread_join(threads[i], NULL);
}

5.3 线程局部存储

c 复制代码
__thread int thread_local_var = 0;  // 每个线程独立的变量

六、完整示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 5

typedef struct {
    int id;
    int iterations;
} thread_data_t;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void *worker_thread(void *arg) {
    thread_data_t *data = (thread_data_t *)arg;
    
    printf("Thread %d started\n", data->id);
    
    for (int i = 0; i < data->iterations; i++) {
        pthread_mutex_lock(&mutex);
        shared_counter++;
        pthread_mutex_unlock(&mutex);
        
        usleep(10000);  // 模拟工作
    }
    
    printf("Thread %d finished\n", data->id);
    
    free(data);  // 释放传入的参数
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    
    // 创建线程
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_data_t *data = malloc(sizeof(thread_data_t));
        data->id = i;
        data->iterations = 100;
        
        if (pthread_create(&threads[i], NULL, worker_thread, data) != 0) {
            fprintf(stderr, "Error creating thread %d\n", i);
            free(data);
            exit(1);
        }
    }
    
    // 等待所有线程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    
    printf("All threads completed. Counter = %d\n", shared_counter);
    
    pthread_mutex_destroy(&mutex);
    return 0;
}

七、调试技巧

7.1 检测竞态条件

使用 Thread Sanitizer:

bash 复制代码
gcc -fsanitize=thread -g program.c -o program -lpthread
./program

7.2 使用GDB调试多线程

bash 复制代码
gdb ./program
(gdb) break thread_func
(gdb) run
(gdb) info threads          # 查看所有线程
(gdb) thread 2              # 切换到线程2
(gdb) where                 # 查看当前线程的调用栈

八、性能考虑

  1. 避免过度加锁: 锁的粒度要合适,太细影响性能,太粗降低并发
  2. 线程数量: 通常设置为CPU核心数或核心数的倍数
  3. 避免频繁创建销毁线程: 考虑使用线程池
  4. 减少锁竞争: 使用无锁数据结构或读写锁

九、Linux线程底层常识

9.1 线程实现模型

Linux线程的本质:

  • Linux内核并不区分线程和进程,统一称为"任务"(task)
  • 线程是通过 clone() 系统调用创建的轻量级进程(LWP)
  • POSIX线程是在用户空间实现的,通过NPTL(Native POSIX Thread Library)

1:1 线程模型:

复制代码
每个用户线程 <---> 一个内核线程(LWP)
  • Linux采用1:1模型,每个pthread对应一个内核调度实体
  • 优点: 真正的并行执行,阻塞不影响其他线程
  • 缺点: 创建开销较大,线程切换有内核开销

9.2 线程ID的真相

两种线程ID:

  1. pthread_t (POSIX线程ID)

    c 复制代码
    pthread_t tid = pthread_self();  // 用户空间的线程标识符
    • 不透明类型,可能是结构体、整数或指针
    • 仅在进程内有效,不同进程中的值可能相同
    • 用于pthread_*函数
  2. LWP ID (轻量级进程ID / 内核线程ID)

    c 复制代码
    #include <sys/syscall.h>
    pid_t tid = syscall(SYS_gettid);  // 内核中的线程ID
    • 系统范围内唯一的整数
    • 可用于CPU亲和性设置、调度策略等
    • top命令显示的就是LWP ID

易错点:

c 复制代码
// ❌ 错误: 不能直接打印pthread_t
printf("%d", pthread_self());  

// ✅ 正确: 获取内核线程ID
printf("LWP ID: %ld", (long)syscall(SYS_gettid));

// ✅ 或者比较pthread_t
if (pthread_equal(tid1, tid2)) { ... }

9.3 线程栈的秘密

默认栈大小:

c 复制代码
#include <pthread.h>
#include <sys/resource.h>

// 查看默认栈大小
pthread_attr_t attr;
size_t stacksize;
pthread_attr_init(&attr);
pthread_attr_getstacksize(&attr, &stacksize);
printf("Default stack: %zu bytes\n", stacksize);  // 通常是8MB

栈溢出陷阱:

c 复制代码
void *thread_func(void *arg) {
    char buffer[10 * 1024 * 1024];  // ❌ 10MB数组,可能栈溢出
    // ... 程序可能崩溃或产生段错误
}

正确做法:

c 复制代码
// 方法1: 使用堆内存
void *thread_func(void *arg) {
    char *buffer = malloc(10 * 1024 * 1024);
    // 使用buffer...
    free(buffer);
}

// 方法2: 增加栈大小
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 16 * 1024 * 1024);  // 16MB
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);

栈地址查看:

c 复制代码
void *thread_func(void *arg) {
    int local_var;
    printf("Stack address: %p\n", (void*)&local_var);
    
    pthread_attr_t attr;
    void *stackaddr;
    size_t stacksize;
    pthread_getattr_np(pthread_self(), &attr);
    pthread_attr_getstack(&attr, &stackaddr, &stacksize);
    printf("Stack: %p - %p (size: %zu)\n", 
           stackaddr, (char*)stackaddr + stacksize, stacksize);
    pthread_attr_destroy(&attr);
}

9.4 线程调度与优先级

调度策略:

Linux支持三种实时调度策略:

  • SCHED_FIFO: 先进先出实时调度
  • SCHED_RR: 时间片轮转实时调度
  • SCHED_OTHER: 普通分时调度(默认)
c 复制代码
#include <sched.h>

// 设置线程调度策略和优先级
pthread_attr_t attr;
struct sched_param param;

pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 50;  // 1-99,数字越大优先级越高
pthread_attr_setschedparam(&attr, &param);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

pthread_create(&tid, &attr, thread_func, NULL);

陷阱:

c 复制代码
// ❌ 忘记设置PTHREAD_EXPLICIT_SCHED,调度策略可能被忽略
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
// 需要root权限或CAP_SYS_NICE能力

9.5 CPU亲和性 (CPU Affinity)

将线程绑定到特定CPU核心:

c 复制代码
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>

void set_cpu_affinity(int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);
    
    pthread_t current = pthread_self();
    pthread_setaffinity_np(current, sizeof(cpu_set_t), &cpuset);
}

void *thread_func(void *arg) {
    int cpu = *(int*)arg;
    set_cpu_affinity(cpu);  // 绑定到指定CPU
    
    // 验证绑定
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
    
    for (int i = 0; i < CPU_SETSIZE; i++) {
        if (CPU_ISSET(i, &cpuset)) {
            printf("Thread running on CPU %d\n", i);
        }
    }
}

应用场景:

  • 高性能计算,减少缓存失效
  • 实时系统,避免线程迁移
  • NUMA架构优化

9.6 信号处理的坑

问题: 信号会被任意线程接收

c 复制代码
// ❌ 危险: 信号可能发送给任意线程
void signal_handler(int sig) {
    printf("Caught signal in thread %ld\n", pthread_self());
}

int main() {
    signal(SIGINT, signal_handler);
    // 创建多个线程...
    // Ctrl+C 时,不确定哪个线程会处理信号
}

正确做法: 屏蔽信号,专门线程处理

c 复制代码
#include <signal.h>

void *signal_thread(void *arg) {
    sigset_t *set = (sigset_t *)arg;
    int sig;
    
    while (1) {
        sigwait(set, &sig);  // 等待信号
        printf("Signal %d received by dedicated thread\n", sig);
        
        if (sig == SIGINT) break;
    }
    return NULL;
}

int main() {
    sigset_t set;
    pthread_t sig_tid;
    
    // 1. 阻塞所有线程的信号
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
    
    // 2. 创建工作线程(继承信号屏蔽字)
    // pthread_create(...);
    
    // 3. 创建专门的信号处理线程
    pthread_create(&sig_tid, NULL, signal_thread, &set);
    
    pthread_join(sig_tid, NULL);
    return 0;
}

9.7 线程取消的陷阱

取消点 (Cancellation Point):

并非所有函数都是取消点,线程不会立即取消:

c 复制代码
void *thread_func(void *arg) {
    // 这个循环可能无法被取消!
    while (1) {
        // 密集计算,没有取消点
    }
}

// 主线程
pthread_cancel(tid);  // 发送取消请求
// 但线程可能永远不会取消!

解决方法:

c 复制代码
// 方法1: 手动添加取消点
void *thread_func(void *arg) {
    while (1) {
        pthread_testcancel();  // 显式取消点
        // 做一些工作...
    }
}

// 方法2: 使用异步取消(危险!)
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

// 方法3: 使用标志位(推荐)
volatile int stop_flag = 0;

void *thread_func(void *arg) {
    while (!stop_flag) {
        // 工作...
    }
}

// 主线程
stop_flag = 1;
pthread_join(tid, NULL);

取消清理处理器:

c 复制代码
void cleanup_handler(void *arg) {
    printf("Cleanup: releasing resource %p\n", arg);
    free(arg);
}

void *thread_func(void *arg) {
    void *resource = malloc(1024);
    
    pthread_cleanup_push(cleanup_handler, resource);  // 注册清理函数
    
    // 可能被取消的代码...
    pthread_testcancel();
    
    pthread_cleanup_pop(1);  // 1=执行清理函数, 0=不执行
    return NULL;
}

9.8 fork() 与多线程的危险组合

重大陷阱: 多线程程序中调用fork()

c 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

void *thread_func(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        counter++;
        pthread_mutex_unlock(&mutex);
        usleep(1000);
    }
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    
    sleep(1);
    
    pid_t pid = fork();  // ⚠️ 危险!
    
    if (pid == 0) {
        // 子进程
        pthread_mutex_lock(&mutex);  // 可能永远死锁!
        printf("Child: %d\n", counter);
        pthread_mutex_unlock(&mutex);
    }
}

问题原因:

  1. fork()后,子进程只复制调用线程,其他线程消失
  2. 如果某个锁在fork时被其他线程持有,子进程中该锁永远无法释放
  3. 子进程继承了父进程的锁状态,但持有锁的线程已不存在

解决方案:

c 复制代码
// 方法1: fork前准备,fork后清理
pthread_atfork(prepare, parent, child);

void prepare(void) {
    pthread_mutex_lock(&mutex);  // fork前获取所有锁
}

void parent(void) {
    pthread_mutex_unlock(&mutex);  // 父进程释放锁
}

void child(void) {
    pthread_mutex_unlock(&mutex);  // 子进程释放锁
}

// 方法2: 子进程fork后立即exec(推荐)
if (fork() == 0) {
    execl("/bin/ls", "ls", NULL);  // 不使用任何父进程资源
    exit(1);
}

// 方法3: 避免在多线程程序中使用fork

9.9 errno 与线程安全

errno 是线程局部的:

c 复制代码
#include <errno.h>

void *thread_func(void *arg) {
    int fd = open("/nonexistent", O_RDONLY);
    if (fd < 0) {
        printf("Thread %ld: errno = %d\n", 
               pthread_self(), errno);  // 每个线程独立的errno
    }
}

但并非所有函数都是线程安全的:

c 复制代码
// ❌ 非线程安全函数
strtok()     // 使用静态缓冲区
localtime()  // 返回静态结构
gethostbyname()

// ✅ 线程安全替代版本
strtok_r()   // reentrant版本
localtime_r()
gethostbyname_r()

识别线程安全函数:

  • 查看man手册的"ATTRIBUTES"部分
  • 通常 _r 后缀表示可重入(reentrant)版本

9.10 内存模型与可见性

问题: 编译器和CPU可能重排序指令

c 复制代码
int data = 0;
int flag = 0;

// 线程1
void *writer(void *arg) {
    data = 42;      // ⚠️ 可能被重排序
    flag = 1;       // 线程2可能先看到flag=1,但data还是0
}

// 线程2
void *reader(void *arg) {
    while (flag == 0) ;  // 自旋等待
    printf("Data: %d\n", data);  // 可能打印0而不是42!
}

解决方案: 使用内存屏障或锁

c 复制代码
// 方法1: 使用互斥锁(隐含内存屏障)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *writer(void *arg) {
    pthread_mutex_lock(&mutex);
    data = 42;
    flag = 1;
    pthread_mutex_unlock(&mutex);  // 确保可见性
}

// 方法2: 使用C11原子操作
#include <stdatomic.h>
atomic_int flag = 0;

void *writer(void *arg) {
    data = 42;
    atomic_store(&flag, 1);  // release语义
}

void *reader(void *arg) {
    while (atomic_load(&flag) == 0) ;  // acquire语义
    printf("Data: %d\n", data);
}

// 方法3: volatile + 内存屏障(GCC)
volatile int flag = 0;

void *writer(void *arg) {
    data = 42;
    __sync_synchronize();  // 内存屏障
    flag = 1;
}

9.11 线程局部存储 (TLS)

三种TLS实现方式:

c 复制代码
// 方法1: __thread关键字(GNU C扩展,最高效)
__thread int thread_id = 0;

void *thread_func(void *arg) {
    thread_id = *(int*)arg;  // 每个线程独立
    printf("Thread %d\n", thread_id);
}

// 方法2: pthread_key_t(POSIX标准,可添加析构函数)
pthread_key_t key;

void destructor(void *value) {
    free(value);
}

int main() {
    pthread_key_create(&key, destructor);
    
    // 每个线程中
    int *data = malloc(sizeof(int));
    pthread_setspecific(key, data);
    
    int *retrieved = pthread_getspecific(key);
}

// 方法3: C11 thread_local关键字
#include <threads.h>
thread_local int counter = 0;

应用场景:

  • 线程特定的错误码
  • 每线程的随机数生成器状态
  • 数据库连接池中的连接
  • 性能计数器

9.12 线程数量的权衡

不是越多越好!

c 复制代码
// ❌ 创建过多线程
for (int i = 0; i < 10000; i++) {
    pthread_create(&tids[i], NULL, task, NULL);
}
// 问题: 上下文切换开销巨大,内存消耗严重

经验法则:

  1. CPU密集型任务: 线程数 = CPU核心数 + 1

    c 复制代码
    int ncores = sysconf(_SC_NPROCESSORS_ONLN);
    int nthreads = ncores + 1;
  2. IO密集型任务: 可以更多,通常2-4倍核心数

    c 复制代码
    int nthreads = ncores * 2;
  3. 使用线程池: 预创建固定数量的线程,复用执行任务

监控线程数量:

bash 复制代码
# 查看进程的线程数
ps -eLf | grep process_name | wc -l

# 或使用top
top -H -p <pid>

# 查看线程详情
cat /proc/<pid>/status | grep Threads

9.13 调试工具

Valgrind - Helgrind (检测竞态条件)

bash 复制代码
valgrind --tool=helgrind ./program

Thread Sanitizer (检测数据竞争)

bash 复制代码
gcc -fsanitize=thread -g program.c -o program -lpthread
./program

GDB多线程调试

bash 复制代码
gdb ./program
(gdb) set scheduler-locking on  # 只运行当前线程
(gdb) thread apply all bt       # 所有线程的调用栈
(gdb) info threads              # 线程列表

SystemTap查看线程活动

bash 复制代码
stap -e 'probe process("/path/to/program").thread.* { println(pp()) }'

十、性能优化技巧

10.1 减少锁竞争

c 复制代码
// ❌ 粗粒度锁
pthread_mutex_lock(&big_lock);
process_data_1();
process_data_2();
pthread_mutex_unlock(&big_lock);

// ✅ 细粒度锁
pthread_mutex_lock(&lock1);
process_data_1();
pthread_mutex_unlock(&lock1);

pthread_mutex_lock(&lock2);
process_data_2();
pthread_mutex_unlock(&lock2);

10.2 读写锁

c 复制代码
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 读多写少的场景
void *reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);  // 多个读者可并发
    // 读取数据
    pthread_rwlock_unlock(&rwlock);
}

void *writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);  // 独占写入
    // 修改数据
    pthread_rwlock_unlock(&rwlock);
}

10.3 自旋锁 vs 互斥锁

c 复制代码
// 短期持有 + 低竞争 -> 自旋锁
pthread_spinlock_t spinlock;
pthread_spin_init(&spinlock, 0);
pthread_spin_lock(&spinlock);
// 快速操作
pthread_spin_unlock(&spinlock);

// 长期持有 + 高竞争 -> 互斥锁
pthread_mutex_lock(&mutex);
// 耗时操作
pthread_mutex_unlock(&mutex);

十一、总结

核心原则:

✅ 编译必须链接 -lpthread

✅ 理解线程=LWP,pthread_t≠内核TID

✅ 栈大小有限,大数组用堆

✅ errno是线程局部的,但非所有函数线程安全

✅ 信号在多线程中要专门处理

✅ 多线程+fork=灾难,避免或用pthread_atfork

✅ 内存可见性需要同步机制保证

✅ 线程取消不一定立即生效

✅ 线程数量要根据任务类型和CPU核心数权衡

✅ 使用工具检测竞态和死锁

调试检查清单:

  • 编译时启用 -Wall -Wextra
  • 使用ThreadSanitizer检测数据竞争
  • 使用Helgrind检测同步错误
  • 检查所有共享变量是否有锁保护
  • 确认joinable线程都被join或detach
  • 验证没有返回栈上变量地址
  • 测试长时间运行是否有资源泄漏
  • 压力测试多线程并发场景

记住: 并发编程的难点不在于创建线程,而在于正确地协调它们!

相关推荐
权泽谦2 小时前
C语言控制台游戏教程:从零实现贪吃蛇(附源码+讲解)
c语言·stm32·游戏
secondyoung3 小时前
Mermaid流程图高效转换为图片方案
c语言·人工智能·windows·vscode·python·docker·流程图
心灵宝贝3 小时前
CentOS 7 安装 unzip-6.0-21.el7.x86_64.rpm 步骤详解(附安装包)
linux·服务器·centos
q***13343 小时前
在linux(Centos)中Mysql的端口修改保姆级教程
linux·mysql·centos
Autism....3 小时前
服务器理解
运维·服务器
Starry_hello world3 小时前
Linux 文件缓冲区
linux
天亮之前_ict3 小时前
【故障排查】intel 服务器安装Win server 2019蓝屏解决方法
运维·服务器
牢七3 小时前
操作系统。
linux
SongYuLong的博客3 小时前
openwrt源码编译环境搭建-安装Luci
linux·嵌入式硬件