多线程编程是现代 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); /* 创建生产者和消费者线程... */ }
线程同步最佳实践
- 最小化临界区 - 只在必须保护的代码段加锁,尽量减少锁持有时间
- 避免死锁 - 多个锁时按固定顺序获取,使用 pthread_mutex_trylock() 检测
- 优先使用高层抽象 - 如线程池、队列等,减少手动同步的复杂性
- 注意锁的粒度 - 粗粒度简单但并发度低,细粒度复杂但性能好
- 初始化与销毁配对 - pthread_mutex_init/destroy、sem_init/destroy 要成对使用
总结
线程同步是多线程编程的核心。互斥锁适合简单互斥,读写锁适合读多写少场景,条件变量适合线程协作,信号量适合计数场景。掌握这些工具,才能写出正确高效的多线程程序。