使用条件变量多线程时出现意外行为

Unexpected behavior while using condition variable multithread

在下面的代码中:

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

pthread_mutex_t mtx;
pthread_cond_t cond;

int how_many = 10;
int pool = 0;

void * producer(void * ptr)
{
        while (how_many > 0)
        {
                pthread_mutex_lock(&mtx);
                printf("producer: %d\n", how_many);
                pool = how_many;
                how_many--;
                pthread_mutex_unlock(&mtx);
                pthread_cond_signal(&cond);
        }

        pthread_exit(0);
}

void * consumer(void * ptr)
{
        while (how_many > 0)
        {
                pthread_mutex_lock(&mtx);
                pthread_cond_wait(&cond, &mtx);
                printf("consumer: %d\n", pool);
                pool = 0;
                pthread_mutex_unlock(&mtx);
        }
        pthread_exit(0);
}

int main(int argc, char ** argv)
{
        pthread_t prod, cons;
        pthread_mutex_init(&mtx, 0);
        pthread_cond_init(&cond, 0);
        pthread_create(&cons, 0, consumer, 0);
        pthread_create(&prod, 0, producer, 0);
        pthread_join(prod, 0);
        pthread_join(cons, 0);
        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mtx);
        return 0;
}

我没有得到预期的输出。

预期输出:

Producer:10    
Consumer:10    
Producer:9    
Consumer:9    
Producer:8    
Consumer:8    
Producer:7    
Consumer:7    
Producer:6    
Consumer:6    
Producer:5    
Consumer:5    
Producer:4    
Consumer:4    
Producer:3    
Consumer:3    
Producer:2    
Consumer:2    
Producer:1    
Consumer:1

实际输出:

producer: 10    
producer: 9    
producer: 8    
producer: 7    
producer: 6    
producer: 5    
producer: 4    
producer: 3    
producer: 2    
producer: 1

另外,在消费者端,如果我们锁定并等待信号,生产者如何获得锁定以便他可以将信号发送给消费者?

  1. 会不会死锁?
  2. 我的朋友们都在推荐pthread_cond_wait(&cond, &mtx);实际上会解锁资源,直到它收到来自生产者的信号。是真的吗?

您正在检查 how_many 锁定的部分。您需要重组代码,以便读取变量被锁覆盖,或者它是 C11 _Atomic.

即便如此,您的代码输出也可能不会是您想要的方式,因为线程的调度几乎不可预测。

对于您预期的输出,您可以使用如下锁定机制,

 #include <pthread.h> 
 #include <stdio.h> 
 #include <stdlib.h> 
 #include <semaphore.h> 

 sem_t mutex1; 
 sem_t mutex2; 
 int main()
 {
    pthread_t thread1, thread2; 
    sem_init(&mutex1, 0, 1); 
    sem_init(&mutex2, 0, 0);
    pthread_create( &thread1, NULL, &producer, NULL)
    pthread_create( &thread2, NULL, &consumer, NULL)
    pthread_join( thread1, NULL); 
    pthread_join( thread2, NULL); 
    return 0; 
 }


void producer()
{
  sem_wait(&mutex1); 
  :
  :

  sem_post(&mutex2);
}

void consumer ()
{
    sem_wait(&mutex2); 
    :
    : 
    sem_post(&mutex1);
}   

互斥锁只提供互斥(如果使用得当);它们本身并不提供阻止特定事件发生或直到满足特定条件的机制。这就是条件变量的用途(和信号量,如果你想走低一点的话)。

您的代码规定消费者可以等待生产者生产,但不能让生产者等待消费者消费后再继续。如果你想让两个线程交替,那么你需要第二个条件变量来为后者提供。

Also, in consumer side, If we lock and wait for the signal, how the producer can get the lock so that he can send the signal to the consumer?

  1. will it be dead lock?

  2. My friends are suggesting like pthread_cond_wait(&cond, &mtx); would actually unlock the resources until it gets the signal from producer. is that true?

您是否考虑过 阅读文档,而不是询问您的朋友或互联网?这是手册页描述它的方式:

These functions atomically release mutex [...]. Upon successful return, the mutex shall have been locked and shall be owned by the calling thread.

也就是说,调用pthread_cond_wait()的线程在等待时并没有持有锁定的互斥量,而是重新获取它之前的互斥量returns(这可能涉及线程接收之间的不确定延迟信号和函数调用返回)。

此外,请始终记住,线程可能会因等待条件变量而意外唤醒。必须在唤醒时检查条件实际上是否满足,如果不满足则继续等待。

您可以通过以下方式构建生产者:

void * producer(void * ptr)
{
        pthread_mutex_lock(&mtx);
        while (how_many > 0)
        {
                if (pool == 0) {
                        printf("producer: %d\n", how_many);
                        pool = how_many;
                        how_many--;
                        pthread_cond_signal(&full_cond);
                }
                pthread_cond_wait(&empty_cond, &mtx);
        }
        pthread_mutex_unlock(&mtx);

        pthread_exit(0);
}

注意:

  1. 我已经重命名了你原来的条件变量并引入了一个新的。现在有 full_cond,表示池(容量 1)已满,empty_cond,表示池为空。
  2. 整个循环受互斥体保护。这很好,因为它执行 pthread_cond_wait() 命名该互斥锁;当生产者等待时,其他线程将能够 运行。互斥锁确保对 how_manypool 变量的访问正确同步。
  3. 循环通过测试 pool 来验证它是否真的是空的来防止虚假唤醒。如果没有,它会循环回等待而不做任何其他事情。
  4. 为了使其正常工作,消费者需要进行相应的更改(留给您作为练习)。