基于Pthread的C语言多线程生产者消费者代码异常求助
原本预期每个生产者线程会独立向缓冲区添加数据,但实际运行时同一个线程持续填充缓冲区,推测是直到发生上下文切换才会切换线程。只能使用pthread的条件变量和互斥锁,不能使用信号量或sleep,请问这是否是问题的原因?运行环境为Ubuntu 22.04下的C语言。
代码实现
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <time.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t fill = PTHREAD_COND_INITIALIZER; pthread_cond_t empty = PTHREAD_COND_INITIALIZER; int buffer[10]; int cnt = 0; // # of items in the buffer int in = 0; // index to add an item to buffer int out = 0; // index to read an item from buffer void *producer(); //producer function void *consumer(); //consumer function int main(void) { srand(time(NULL)); pthread_t Pthread[5]; pthread_t Cthread[5]; for (int i=0; i<5; i++) { pthread_create(&Pthread[i], NULL, producer, NULL); } for (int i=0; i<5; i++) { pthread_create(&Cthread[i], NULL, consumer, NULL); } for (int i=0; i<5; i++) { pthread_join(Pthread[i], NULL); } for (int i=0; i<5; i++) { pthread_join(Cthread[i], NULL); } return 0; } void *producer(void *arg){ while(1){ int next_produced = rand(); // create a random integer pthread_mutex_lock(&mutex); //======== critical section ============= while (cnt == 10) { pthread_cond_wait(&empty, &mutex); } buffer[in] = next_produced; // add the item in = (in + 1) % 10; // move to next index cnt++; printf("Producer thread: %lu adds the value : %d\n", pthread_self(), next_produced); pthread_cond_signal(&fill); //======== critical section ============= pthread_mutex_unlock(&mutex); } } void *consumer(void *arg){ while(1){ pthread_mutex_lock(&mutex); //======== critical section ============= while (cnt == 0){ pthread_cond_wait(&fill, &mutex); } int next_consumed = buffer[out]; // read an item from buffer buffer[out] = 0; // remove the item from buffer out = (out + 1) % 10; // move to next index cnt--; printf("Consumer thread: %lu deletes the value : %d\n", pthread_self(),next_consumed); pthread_cond_signal(&empty); //======== critical section ============= pthread_mutex_unlock(&mutex); } }
部分运行结果
Producer thread: 140109028513344 adds the value : 1457620057 Producer thread: 140109028513344 adds the value : 1109246666 Producer thread: 140109028513344 adds the value : 34472492 Producer thread: 140109028513344 adds the value : 1658551151 Producer thread: 140109028513344 adds the value : 1646031817 Producer thread: 140109028513344 adds the value : 2114864384 Producer thread: 140109028513344 adds the value : 2019817090 Producer thread: 140109028513344 adds the value : 90771015 Producer thread: 140109028513344 adds the value : 1977802548 Producer thread: 140109028513344 adds the value : 1337378708 Consumer thread: 140108921554496 deletes the value : 1457620057 Consumer thread: 140108921554496 deletes the value : 1109246666 Consumer thread: 140108921554496 deletes the value : 34472492 Consumer thread: 140108921554496 deletes the value : 1658551151 Consumer thread: 140108921554496 deletes the value : 1646031817 Consumer thread: 140108921554496 deletes the value : 2114864384 Consumer thread: 140108921554496 deletes the value : 2019817090 Consumer thread: 140108921554496 deletes the value : 90771015 Consumer thread: 140108921554496 deletes the value : 1977802548 Consumer thread: 140108921554496 deletes the value : 1337378708 Producer thread: 140109036906048 adds the value : 662295424 Consumer thread: 140108913161792 deletes the value : 662295424 Producer thread: 140108946732608 adds the value : 1952746478 Producer thread: 140108946732608 adds the value : 1782607749
你的推测部分正确,但还有一个关键代码问题共同导致了这个现象:
1. 操作系统线程调度特性是直接诱因
Linux的线程调度器默认会尽量减少上下文切换的开销,当一个线程释放互斥锁后,如果它很快再次尝试获取锁,调度器大概率会让它继续持有CPU——因为切换线程需要保存/恢复寄存器等状态,代价较高。所以单个生产者线程会连续获取锁,填充缓冲区直到触发阻塞(比如缓冲区满)或者调度器强制进行上下文切换。
2. pthread_cond_signal的使用放大了这个问题
你当前使用pthread_cond_signal只会唤醒一个等待在对应条件变量上的线程:
- 生产者添加数据后调用
signal,只会唤醒一个等待的消费者; - 消费者取走数据后调用
signal,也只会唤醒一个等待的生产者。
如果被唤醒的刚好是刚才释放锁的那个线程(调度器倾向于这样),就会导致该线程再次抢到锁,继续执行,进一步加剧了单个线程连续运行的情况。
修复方案
把代码中的pthread_cond_signal替换成pthread_cond_broadcast:
- 生产者中的
pthread_cond_signal(&fill);改成pthread_cond_broadcast(&fill); - 消费者中的
pthread_cond_signal(&empty);改成pthread_cond_broadcast(&empty);
pthread_cond_broadcast会唤醒所有等待在条件变量上的线程,这样多个等待的生产者/消费者都会被唤醒参与锁竞争,调度器有更大的概率选择不同的线程执行,从而避免单个线程持续填充缓冲区的情况。
注意:即使修改后,调度器仍可能偶尔让同一个线程连续抢到锁,但整体上多线程的执行会更加均衡。另外,rand()函数并非线程安全,若需线程安全的随机数生成,可以使用rand_r()或者arc4random()。
内容的提问来源于stack exchange,提问作者wdh4804

