C语言线程同步完全指南

多线程编程是现代 C 语言开发中的重要技能。本文将详细介绍 pthread 库中的线程同步机制,包括互斥锁、读写锁、条件变量与信号量。

为什么需要线程同步?

多个线程共享进程地址空间,如果同时访问共享资源而不加保护,就会导致数据竞争(data race),产生不可预期的结果。典型场景包括:

  • 多个线程同时修改同一个全局变量
  • 多个线程同时访问和修改链表、数组等数据结构
  • 生产者-消费者问题中的缓冲区操作
  • 读者-写者问题中的文件/数据库访问

互斥锁(Mutex)

互斥锁是最基本的同步机制,确保同一时刻只有一个线程能够进入临界区:

mutex_demo.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);    /* 加锁 */
        counter++;                              /* 临界区 */
        pthread_mutex_unlock(&mutex);  /* 解锁 */
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter: %d\n", counter);  /* 期望: 200000 */
    pthread_mutex_destroy(&mutex);
    return 0;
}

读写锁(Read-Write Lock)

当读多写少时,读写锁比互斥锁更高效:多个线程可以同时读,但写操作需要独占:

rwlock_demo.c
#include <stdio.h>
#include <pthread.h>

typedef struct {
    pthread_rwlock_t lock;
    int data;
} Data;

void read_data(Data *d) {
    pthread_rwlock_rdlock(&d->lock);  /* 读锁 */
    printf("Read: %d\n", d->data);
    pthread_rwlock_unlock(&d->lock);
}

void write_data(Data *d, int val) {
    pthread_rwlock_wrlock(&d->lock);  /* 写锁 */
    d->data = val;
    printf("Write: %d\n", val);
    pthread_rwlock_unlock(&d->lock);
}

条件变量(Condition Variable)

条件变量用于线程间的协作等待,让线程在某个条件满足之前阻塞:

condvar_demo.c
#include <stdio.h>
#include <pthread.h>

typedef struct {
    pthread_mutex_t mutex;
    pthread_cond_t  cond;
    int ready;
} Queue;

void push(Queue *q, int val) {
    pthread_mutex_lock(&q->mutex);
    /* 添加数据到队列 */
    q->ready = 1;
    pthread_cond_signal(&q->cond);  /* 通知消费者 */
    pthread_mutex_unlock(&q->mutex);
}

void *consumer(void *arg) {
    Queue *q = (Queue *)arg;
    pthread_mutex_lock(&q->mutex);
    while (!q->ready) {
        pthread_cond_wait(&q->cond, &q->mutex);  /* 等待数据 */
    }
    printf("Consumed!\n");
    pthread_mutex_unlock(&q->mutex);
    return NULL;
}

信号量(Semaphore)

信号量是一种更通用的同步原语,可用于实现互斥锁和计数:

semaphore_demo.c
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define BUF_SIZE 5
int buffer[BUF_SIZE];
int count = 0;

sem_t empty;  /* 空槽数量 */
sem_t full;   /* 满槽数量 */
pthread_mutex_t mutex;

void *producer(void *arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&empty);      /* 等待空槽 */
        pthread_mutex_lock(&mutex);
        buffer[count++] = i;
        printf("Producer: %d\n", i);
        pthread_mutex_unlock(&mutex);
        sem_post(&full);       /* 增加满槽 */
    }
    return NULL;
}

void *consumer(void *arg) {
    for (int i = 0; i < 10; i++) {
        sem_wait(&full);       /* 等待满槽 */
        pthread_mutex_lock(&mutex);
        int val = buffer[--count];
        printf("Consumer: %d\n", val);
        pthread_mutex_unlock(&mutex);
        sem_post(&empty);      /* 增加空槽 */
    }
    return NULL;
}

int main(void) {
    sem_init(&empty, 0, BUF_SIZE);
    sem_init(&full, 0, 0);
    pthread_mutex_init(&mutex, NULL);
    /* 创建生产者和消费者线程... */
}

线程同步最佳实践

  1. 最小化临界区 - 只在必须保护的代码段加锁,尽量减少锁持有时间
  2. 避免死锁 - 多个锁时按固定顺序获取,使用 pthread_mutex_trylock() 检测
  3. 优先使用高层抽象 - 如线程池、队列等,减少手动同步的复杂性
  4. 注意锁的粒度 - 粗粒度简单但并发度低,细粒度复杂但性能好
  5. 初始化与销毁配对 - pthread_mutex_init/destroy、sem_init/destroy 要成对使用

总结

线程同步是多线程编程的核心。互斥锁适合简单互斥,读写锁适合读多写少场景,条件变量适合线程协作,信号量适合计数场景。掌握这些工具,才能写出正确高效的多线程程序。