在多线程编程中,线程同步是确保多个线程正确协作的关键技术。当多个线程访问共享资源时,如果没有适当的同步机制,可能会导致数据竞争、死锁等问题。本文将详细介绍C语言中常用的线程同步技术。
为什么需要线程同步?
想象一下银行账户操作的经典例子:
- 线程A:读取余额(100元) → 存入50元 → 写入新余额(150元)
- 线程B:读取余额(100元) → 取出30元 → 写入新余额(70元)
如果没有同步,最终余额可能是70元(线程B覆盖了线程A的修改),而不是正确的120元。这就是典型的数据竞争问题。
基本的同步机制
1. 互斥锁 (Mutex)
互斥锁是最基本的同步机制,用于保护临界区,确保同一时间只有一个线程可以访问共享资源。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;void* increment_thread(void* arg) {for (int i = 0; i < 5; i++) {pthread_mutex_lock(&mutex); // 加锁// 临界区开始int temp = shared_counter;printf("Thread %ld: read value = %d\n", (long)arg, temp);usleep(1000); // 模拟一些处理时间shared_counter = temp + 1;printf("Thread %ld: updated value = %d\n", (long)arg, shared_counter);// 临界区结束pthread_mutex_unlock(&mutex); // 解锁usleep(10000); // 让出CPU给其他线程}return NULL;
}int main() {pthread_t t1, t2, t3;// 创建三个线程pthread_create(&t1, NULL, increment_thread, (void*)1);pthread_create(&t2, NULL, increment_thread, (void*)2);pthread_create(&t3, NULL, increment_thread, (void*)3);// 等待所有线程完成pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);printf("Final counter value: %d (expected: 15)\n", shared_counter);pthread_mutex_destroy(&mutex); // 销毁互斥锁return 0;
}
2. 条件变量 (Condition Variables)
条件变量用于线程间的通信,允许线程等待特定条件发生。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>#define BUFFER_SIZE 5pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;int buffer[BUFFER_SIZE];
int count = 0; // 缓冲区中元素数量
int in = 0; // 生产者插入位置
int out = 0; // 消费者取出位置void* producer(void* arg) {int item;for (int i = 0; i < 10; i++) {item = rand() % 100; // 生产一个随机数pthread_mutex_lock(&mutex);// 如果缓冲区满,等待消费者消费while (count == BUFFER_SIZE) {printf("Producer: buffer full, waiting...\n");pthread_cond_wait(&cond_producer, &mutex);}// 生产物品buffer[in] = item;in = (in + 1) % BUFFER_SIZE;count++;printf("Producer: produced item %d, count = %d\n", item, count);pthread_cond_signal(&cond_consumer); // 通知消费者pthread_mutex_unlock(&mutex);usleep(rand() % 100000);}return NULL;
}void* consumer(void* arg) {int item;for (int i = 0; i < 10; i++) {pthread_mutex_lock(&mutex);// 如果缓冲区空,等待生产者生产while (count == 0) {printf("Consumer: buffer empty, waiting...\n");pthread_cond_wait(&cond_consumer, &mutex);}// 消费物品item = buffer[out];out = (out + 1) % BUFFER_SIZE;count--;printf("Consumer: consumed item %d, count = %d\n", item, count);pthread_cond_signal(&cond_producer); // 通知生产者pthread_mutex_unlock(&mutex);usleep(rand() % 100000);}return NULL;
}int main() {pthread_t prod, cons;srand(time(NULL));pthread_create(&prod, NULL, producer, NULL);pthread_create(&cons, NULL, consumer, NULL);pthread_join(prod, NULL);pthread_join(cons, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond_producer);pthread_cond_destroy(&cond_consumer);return 0;
}
3. 读写锁 (Read-Write Lock)
读写锁允许多个读操作同时进行,但写操作需要独占访问。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;
int readers_count = 0;void* reader(void* arg) {int id = (long)arg;for (int i = 0; i < 5; i++) {pthread_rwlock_rdlock(&rwlock); // 获取读锁readers_count++;printf("Reader %d: data = %d (total readers: %d)\n", id, shared_data, readers_count);usleep(50000); // 模拟读取时间readers_count--;pthread_rwlock_unlock(&rwlock);usleep(100000);}return NULL;
}void* writer(void* arg) {int id = (long)arg;for (int i = 0; i < 3; i++) {pthread_rwlock_wrlock(&rwlock); // 获取写锁shared_data++;printf("Writer %d: updated data to %d\n", id, shared_data);usleep(100000); // 模拟写入时间pthread_rwlock_unlock(&rwlock);usleep(200000);}return NULL;
}int main() {pthread_t readers[3], writers[2];// 创建读者线程for (long i = 0; i < 3; i++) {pthread_create(&readers[i], NULL, reader, (void*)(i + 1));}// 创建写者线程for (long i = 0; i < 2; i++) {pthread_create(&writers[i], NULL, writer, (void*)(i + 1));}// 等待所有线程完成for (int i = 0; i < 3; i++) {pthread_join(readers[i], NULL);}for (int i = 0; i < 2; i++) {pthread_join(writers[i], NULL);}pthread_rwlock_destroy(&rwlock);return 0;
}
4. 信号量 (Semaphore)
信号量用于控制对有限资源的访问。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>#define NUM_CHAIRS 3sem_t barber_ready; // 理发师是否准备好
sem_t customer_ready; // 顾客是否准备好
sem_t access_chairs; // 保护椅子访问的互斥信号量int waiting_customers = 0;void* barber(void* arg) {while (1) {printf("Barber: sleeping...\n");sem_wait(&customer_ready); // 等待顾客sem_wait(&access_chairs);waiting_customers--;sem_post(&barber_ready); // 通知理发师准备好了sem_post(&access_chairs);printf("Barber: cutting hair...\n");usleep(200000); // 理发时间printf("Barber: finished cutting hair\n");}return NULL;
}void* customer(void* arg) {int id = (long)arg;sem_wait(&access_chairs);if (waiting_customers < NUM_CHAIRS) {waiting_customers++;printf("Customer %d: took a seat, waiting customers: %d\n", id, waiting_customers);sem_post(&customer_ready); // 通知理发师有顾客sem_post(&access_chairs);sem_wait(&barber_ready); // 等待理发师printf("Customer %d: getting haircut\n", id);} else {sem_post(&access_chairs);printf("Customer %d: no seats available, leaving\n", id);}return NULL;
}int main() {pthread_t barber_thread, customer_threads[10];// 初始化信号量sem_init(&barber_ready, 0, 0);sem_init(&customer_ready, 0, 0);sem_init(&access_chairs, 0, 1);pthread_create(&barber_thread, NULL, barber, NULL);// 创建顾客线程for (long i = 0; i < 10; i++) {pthread_create(&customer_threads[i], NULL, customer, (void*)(i + 1));usleep(100000); // 顾客陆续到达}// 等待所有顾客完成for (int i = 0; i < 10; i++) {pthread_join(customer_threads[i], NULL);}// 清理资源sem_destroy(&barber_ready);sem_destroy(&customer_ready);sem_destroy(&access_chairs);return 0;
}
同步机制对比
机制 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
互斥锁 | 保护临界区,防止数据竞争 | 简单易用,性能较好 | 可能引起死锁 |
条件变量 | 线程间通信,等待特定条件 | 高效的事件通知机制 | 必须与互斥锁配合使用 |
读写锁 | 读多写少的场景 | 允许多个读操作并发 | 写操作可能饿死 |
信号量 | 资源池管理,生产者消费者 | 灵活的计数机制 | 使用相对复杂 |
避免常见陷阱
1. 死锁预防
// 错误的做法:可能导致死锁
void transfer_wrong(account_t* a, account_t* b, int amount) {pthread_mutex_lock(&a->mutex);pthread_mutex_lock(&b->mutex);// 转账操作...pthread_mutex_unlock(&b->mutex);pthread_mutex_unlock(&a->mutex);
}// 正确的做法:按固定顺序加锁
void transfer_correct(account_t* a, account_t* b, int amount) {// 按地址顺序加锁,避免死锁if (a < b) {pthread_mutex_lock(&a->mutex);pthread_mutex_lock(&b->mutex);} else {pthread_mutex_lock(&b->mutex);pthread_mutex_lock(&a->mutex);}// 转账操作...pthread_mutex_unlock(&b->mutex);pthread_mutex_unlock(&a->mutex);
}
2. 条件变量的正确使用
// 错误的做法:可能错过信号
while (!condition) {pthread_cond_wait(&cond, &mutex);
}// 正确的做法:使用while循环检查条件
pthread_mutex_lock(&mutex);
while (!condition) { // 必须用while,不能用ifpthread_cond_wait(&cond, &mutex);
}
// 处理条件满足的情况
pthread_mutex_unlock(&mutex);
性能优化建议
- 减小锁的粒度:尽量缩短持有锁的时间
- 使用读写锁替代互斥锁:在读多写少的场景下
- 避免锁的嵌套:减少死锁风险
- 考虑无锁数据结构:对于性能要求极高的场景
总结
C语言的多线程同步机制虽然相对底层,但提供了强大的灵活性。通过合理使用互斥锁、条件变量、读写锁和信号量,可以构建出高效、安全的并发程序。关键在于理解每种同步机制的特性和适用场景,避免常见的陷阱如死锁和竞态条件。
在实际开发中,建议先从简单的互斥锁开始,根据具体需求逐步引入更复杂的同步机制。同时,要养成良好的编程习惯,如及时释放锁、正确使用条件变量等,这样才能写出稳健的多线程程序。