Потребитель не запустится, пока массив не заполнится - PullRequest
1 голос
/ 17 апреля 2020

Как видно из заголовка, у меня проблема с тем, что потребительский поток ожидает, пока весь массив не будет заполнен, пока он не начнет потреблять, затем производитель ждет, пока он снова не опустеет, и округляет их go по кругу, пока они закончили со своими петлями. Я понятия не имею, почему они это делают. Будьте осторожны, так как это новый топи c для меня, и я пытаюсь понять мьютексы и условия.

#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;

}

Ответы [ 2 ]

1 голос
/ 17 апреля 2020

Тезис о том, что потоки производителя и потребителя чередуются BUFFER_SIZE раз, неверен. Программа здесь демонстрирует недетерминированность, поэтому возможен один из многих заказов между производителем и потребителем. То, как написана программа, гарантирует только две вещи:

  1. Если потребитель получает блокировку, когда буфер пуст, он снимет sh блокировку и будет ждать, пока производитель сообщит о ней.
  2. Если производитель получит блокировку при заполнении буфера, он снимет блокировку sh и будет ждать сигнала от потребителя.

Как следствие этих двух свойств Гарантируется, что производитель всегда будет излучать первым, а потребитель всегда будет последним из двух потоков для печати. Это также гарантирует, что ни один из потоков не сможет успешно запросить блокировку более чем BUFFER_SIZE раз подряд.

Помимо двух вышеупомянутых гарантий, фактический запуск даст полностью результаты, определенные узлом c. Случилось так, что ваша операционная система произвольно решила повторно запланировать последний поток в ваших наблюдаемых запусках. Это разумно, потому что ваша программа сказала планировщику: «делайте все, что хотите, в пределах ограничений порядка двух правил». ОС может свободно смещаться в сторону планирования того же потока для повторного запуска на ЦП, если она того пожелает; на самом деле, это, вероятно, наиболее целесообразно, поскольку поток, только что запущенный на ЦП, уже загрузил свои ресурсы ( локальность ссылки ), поэтому накладные расходы от переключений контекста уменьшаются.

Несмотря на то, что ОС может планировать один и тот же поток несколько раз, также возможно, что, например, весь процесс отменен по расписанию и запущен процесс с более высоким приоритетом, потенциально высвобождая рабочий набор из первый процесс. В этом сценарии, когда первый процесс перепланирован, любой поток может быть так же хорош, как и план.

В любом случае, когда программист допускает недетерминированность, упорядочение выходит за пределы допустимого. hands и могут показаться не случайными по ряду сложных причин.

Как я упоминал в комментариях, важно иметь возможность убедиться в свойствах упорядочения программы, не запуская программа. Запуск программы может доказать, что недетерминизм существует, но он не может доказать, что программа детерминирована c. Некоторые многопоточные программы содержат незначительные ошибки планирования или состояния гонки, которые могут возникать только один раз за триллион (или больше!) Запусков, поэтому нет возможности вручную проверить такие недетерминированные c программы. К счастью, этот тривиален, поэтому его легко запустить до появления недетерминизма.

Полезный инструмент для отладки многопоточных программ - sleep(1) в unistd.h. Эта функция вызывает отмену планирования вызывающего потока, нарушая естественное упорядочение программы и вызывая определенное упорядочение. Это может помочь вам доказать заказ недвижимости. Например, добавление sleep(1) после pthread_cond_signal(&emptyCond); показывает, что при данной возможности потребитель захватит блокировку до того, как в вашей программе произойдет BUFFER_SIZE.

Для сложных программ такие инструменты, как Cuzz существует для программной вставки sleep вызовов для выявления ошибок в заказе. См. подход к тестированию для многопоточного программного обеспечения для разнообразия ресурсов и стратегий.

0 голосов
/ 17 апреля 2020

Вам следует подумать, действительно ли блокировка мьютекса является именно тем примитивом синхронизации, который вам действительно нужен. Некоторые альтернативы, которые вы могли бы рассмотреть:

  • Имеют два буфера, один для чтения и один для записи. Поток производителя всегда имеет свободный буфер для обновления. Оба потока ждут на барьере, когда они закончат с буфером, который они в настоящее время имеют, затем поменяют, какой буфер читается, а какой записывается.
  • Поток модуля записи управляет буферами. Он уведомляет поток считывателя о том, что данные готовы в новом буфере, обновляя указатель на буфер (с помощью команды atomi c сравнивать-менять-менять в порядке получения и потребления памяти). Поток считывателя уведомляет записывающего, что он готов получить больше данных, путем очистки указателя буфера, что позволяет успешно выполнить операцию CAS писателя. (Если имеется более двух потоков, этот простой механизм больше не работает и требуется отдельное количество считывателей, а также несколько дополнительных битов тега, чтобы предотвратить ошибки ABA, когда средство записи повторно использует буфер.)
  • Существует круговой буфер элементов atomi c, к которым можно обращаться по отдельности, и читатель и писатель обновляют свои текущие позиции в общей памяти. Поток читателя не читает мимо того места, где он написал другой поток, а поток записи не пишет мимо того, что прочитал другой поток. Ограждения памяти обеспечивают согласованность.
  • Поток модуля записи добавляет каждый записываемый блок в список буферов без ожидания, который использует поток программы чтения.
...