消费者在数组满之前不会启动

Consumer won't start until array is full

正如标题所说,我遇到了一个问题,消费者线程等待整个数组被填满,直到它开始消费,然后生产者等待直到它再次为空,然后他们绕圈子直到他们'完成了他们的循环。我不知道他们为什么要那样做。请保持温和,因为这对我来说是一个新主题,我正在尝试了解互斥锁和条件语句。

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

#define BUFFER_SIZE 6
#define LOOPS 40

char buff[BUFFER_SIZE];
pthread_mutex_t buffLock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t emptyCond=PTHREAD_COND_INITIALIZER, fullCond=PTHREAD_COND_INITIALIZER;
int buffIndex=0;

void* Producer(){
 int i=0;
 for(i=0;i<LOOPS;i++){
    pthread_mutex_lock(&buffLock);
    while(buffIndex==BUFFER_SIZE)
        pthread_cond_wait(&fullCond, &buffLock);

    buff[buffIndex++]=i;
    printf("Producer made: %d\n", i);
    pthread_mutex_unlock(&buffLock);
    pthread_cond_signal(&emptyCond);
 }

pthread_exit(0);
}

void* Consumer(){
 int j=0, value=0;
 for(j=0;j<LOOPS;j++){
    pthread_mutex_lock(&buffLock);
    while(buffIndex==0)
        pthread_cond_wait(&emptyCond, &buffLock);

 value=buff[--buffIndex];
 printf("Consumer used: %d\n", value);
 pthread_mutex_unlock(&buffLock);
 pthread_cond_signal(&fullCond);
 }

pthread_exit(0);
}

int main(){
 pthread_t prodThread, consThread;

 pthread_create(&prodThread, NULL, Producer, NULL);
 pthread_create(&consThread, NULL, Consumer, NULL);

 pthread_join(prodThread, NULL);
 printf("Producer finished.\n");
 pthread_join(consThread, NULL);
 printf("Consumer finished.\n");

 return 0;

}

生产者和消费者线程交替 运行ning BUFFER_SIZE 次的论点是不正确的。这里的程序表现出不确定性,因此生产者和消费者之间可能有许多顺序之一。程序的编写方式只保证了两件事:

  1. 如果消费者在缓冲区为空时获得锁,它将放弃锁并等待生产者发出信号。
  2. 如果生产者在缓冲区已满时获得锁,它将放弃锁并等待消费者发出信号。

由于这两个属性,可以保证生产者始终先发出,而消费者始终是两个线程中的最后一个进行打印。它还保证两个线程都不能连续成功地申请锁超过 BUFFER_SIZE 次。

除了上述两个保证之外,实际的 运行 将产生完全节点确定性的结果。碰巧你的操作系统已经任意决定在你观察到的 运行 中反复重新安排最新的线程。这是合理的,因为你的程序已经对调度程序说 "do whatever you want within the ordering constraints of above two rules"。 OS 可以自由偏向 scheduling the same thread to run on the CPU again if it wants; in fact, this probably makes the most sense as the thread that just ran on the CPU has its resources already loaded (locality of reference), so overhead from context switches 减少。

虽然 OS 可能会重复调度同一个线程,但也有可能,例如,整个进程被取消调度并且更高优先级的进程是 运行,可能会驱逐 working set 的第一个过程。在这种情况下,当第一个进程被重新调度时,任何线程都可能有机会被调度。

无论如何,只要程序员允许不确定性,排序就超出了他们的控制,may not appear to be random 出于各种复杂的原因。

正如我在评论中提到的,重要的是能够在没有 运行 运行程序的情况下让自己相信程序的排序属性。 运行程序可以证明非确定性存在,但不能证明程序是确定性的。一些多线程程序包含微妙的调度错误或竞争条件,这些错误或竞争条件可能仅在一万亿(或更多!)运行 秒中出现一次,因此无法手动检查此类不确定的程序。幸运的是,这个是微不足道的,所以很容易 运行 它直到出现不确定性。

调试多线程程序的有用工具是 unistd.h 中的 sleep(1)。此函数导致调用线程被取消调度,扰乱程序的自然顺序并强制执行特定顺序。这可以帮助您证明排序属性。例如,在 pthread_cond_signal(&emptyCond); 之后添加一个 sleep(1) 表示如果有机会,消费者将在您的程序中发生 BUFFER_SIZE 生产之前获取锁。

对于复杂的程序,Cuzz exist to programmatically insert sleep calls to uncover ordering bugs. See testing approach for multi-threaded software 等工具可用于各种资源和策略。

您应该考虑互斥锁是否是您真正想要的同步原语。您可能会考虑的一些替代方案是:

  • 有两个缓冲区,一个正在读取,一个正在写入。生产者线程总是有一个空闲缓冲区可以更新。当两个线程完成当前拥有的缓冲区时,它们都在屏障上等待,然后交换正在读取的缓冲区和正在写入的缓冲区。
  • 写入线程管理缓冲区。它通过更新指向缓冲区的指针通知 reader 线程数据已在新缓冲区中准备就绪(使用获取-使用内存排序中的原子比较和交换)。 reader 线程通过清除缓冲区指针通知写入器它已准备好接收更多数据,这允许写入器的 CAS 操作成功。 (如果有两个以上的线程,这个简单的机制就不再有效,需要有一个单独的 readers 计数,以及一些额外的标记位,以防止编写者重新使用缓冲区时出现 A-B-A 错误.)
  • 有一个原子元素的循环缓冲区,可以单独访问,reader和writer更新它们在共享内存中的当前位置。 reader 线程不会读取其他线程已写入的内容,并且写入线程不会写入其他线程已读取的内容。内存栅栏确保一致性。
  • 写入器线程将它写入的每个块添加到缓冲区的无等待列表,reader 线程使用该列表。