Вопрос или проблема
Этот код имитирует раздачу супа, предназначенную для равномерного распределения супа среди всех, кто получает его. Он тестируется с 20 людьми, которые должны получить по 1000 тарелок супа каждый. Вывод выглядит правильным примерно в одном из двадцати случаев, но в остальных случаях он раздает весь суп первым 3-4 людям и никому из остальных или просто дает всем случайное количество.
Я считаю, что этот код вызывает конкурентные условия из-за неправильного использования моих мьютексов. Однако все общие переменные защищены, поэтому я не уверен, как это может происходить.
К раздаче супа повторно обращается класс Person, который не защищен мьютексом.
Soupline.cpp:
#include <algorithm>
#include <mutex>
#include <iostream>
#include "../includes/Soupline.h"
#include "../includes/constants.h"
#include "../includes/externs.h"
Soupline::Soupline(int numbBowlsSoup, int numbDrinks) : numbBowlsSoupLeft(numbBowlsSoup), numbDrinksLeft(numbDrinks) {}
int Soupline::getFewestBowlsOfSoupServedToACustomer() {
if(my_customers.empty()) {
return ZERO;
}
//наименьшее значение в векторе
auto min_bowl_customer = std::min_element(my_customers.begin(), my_customers.end(),[](const customer& a, const customer& b) {
return a.numbBowlsSoup < b.numbBowlsSoup;
});
std::cout<<"Наименьшее количество тарелок: "<<min_bowl_customer->numbBowlsSoup<<std::endl;
return min_bowl_customer->numbBowlsSoup;
}
int Soupline::getFewestDrinksServedToACustomer() {
//std::lock_guard<std::mutex> lock(my_customers_mutex);
if (my_customers.empty()) {
return ZERO;
}
auto min_drink_customer = std::min_element(my_customers.begin(), my_customers.end(),[](const customer& a, const customer& b) {
return a.numbDrinks < b.numbDrinks;
});
std::cout<<"Наименьшее количество напитков: "<<min_drink_customer->numbDrinks<<std::endl;
return min_drink_customer->numbDrinks;
}
void Soupline::restock(int numbBowlsSoup, int numbDrinks) {
std::lock_guard<std::mutex> soupLock(soup_mutex);
std::lock_guard<std::mutex> drinkLock(drink_mutex);
numbBowlsSoupLeft = numbBowlsSoup;
numbDrinksLeft = numbDrinks;
}
int Soupline::getSoup(int personID) {
std::lock_guard<std::mutex> lock(soup_mutex);
if(numbBowlsSoupLeft == 0) {
return OUT_OF_SOUP;
}
// проверьте, есть ли ID в векторе людей
auto current = std::find_if(my_customers.begin(), my_customers.end(), [personID](const customer& customer) {
return customer.personID == personID;
});
if (current == my_customers.end() && numbBowlsSoupLeft > 0) {
customer new_customer;
new_customer.personID = personID;
new_customer.numbBowlsSoup = 1;
numbBowlsSoupLeft --;
my_customers.push_back(new_customer);
std::cout<<"КЛИЕНТ ДОБАВЛЕН"<<std::endl;
return BOWL_OF_SOUP;
}
int leastBowls = getFewestBowlsOfSoupServedToACustomer();
if (current->numbBowlsSoup > leastBowls) {
std::cout<<"НЕ ВАШЕ ВРЕМЯ"<<std::endl;
return NOT_YOUR_TURN;
}
current->numbBowlsSoup ++;
numbBowlsSoupLeft --;
// std::cout<<"Осталось супа:"<<numbBowlsSoupLeft<<std::endl;
return BOWL_OF_SOUP;
}
int Soupline::getDrink(int personID) {
std::lock_guard<std::mutex> lock(drink_mutex);
if(numbDrinksLeft == 0) {
return OUT_OF_DRINKS;
}
// проверьте, есть ли ID в векторе людей
auto current = std::find_if(my_customers.begin(), my_customers.end(), [personID](const customer& c) {
return c.personID == personID;
});
if (current == my_customers.end() && numbDrinksLeft > 0) {
customer new_customer;
new_customer.personID = personID;
new_customer.numbDrinks = 1;
numbDrinksLeft -= 1;
my_customers.push_back(new_customer);
return DRINK;
}
int leastDrinks = getFewestDrinksServedToACustomer();
if (current->numbDrinks > leastDrinks) {
return NOT_YOUR_TURN;
}
current->numbDrinks ++;
numbDrinksLeft --;
return DRINK;
}
Person.cpp:
#include <iostream>
#include <thread>
#include <vector>
#include <time.h>
#include <chrono> // this_thread::sleep_for(chrono::milliseconds(MILLI_SECONDS_TO_WAIT));
#include "../includes/Person.h"
#include "../includes/constants.h"
#include "../includes/externs.h"
#include "../includes/Soupline.h"
using namespace std;
Person::Person(int personID) : personID(personID), numbBowlsSoupEaten(0), numbDrinksDrunk(0) {}
void Person::eatlunch(){
bool askForSoup = msl.getnumbBowlsLeft() > 0;
bool askForDrinks = msl.getnumbDrinksLeft() > 0;
while(askForSoup || askForDrinks) {
//std::lock_guard<std::mutex> lock(my_customers_mutex);
if(askForSoup) {
//std::cout<<"СПРАШИВАЕТ СУП"<<std::endl;
//std::lock_guard<std::mutex> soupLock(soup_mutex);
int soup_return = msl.getSoup(personID);
//std::cout<<"ВОЗВРАЩАЕТ "<<soup_return<<std::endl;
if(soup_return == NOT_YOUR_TURN) {
std::this_thread::sleep_for(std::chrono::milliseconds(MILLI_SECONDS_TO_WAIT));
//std::cout<<"НЕ ВАШЕ ВРЕМЯ ДЛЯ СУПА"<<std::endl;
}
else if(soup_return == BOWL_OF_SOUP) {
numbBowlsSoupEaten ++;
//std::cout<<"СУП СЪЕДЕН клиентом "<<personID<<std::endl;
}
else if (soup_return == OUT_OF_SOUP) {
askForSoup = false;
//std::cout<<"НЕТ СУПА"<<std::endl;
}
}
if(askForDrinks) {
//std::cout<<"СПРАШИВАЕТ НАПИТКИ"<<std::endl;
//std::lock_guard<std::mutex> drinkLock(drink_mutex);
int drink_return = msl.getDrink(personID);
if(drink_return == NOT_YOUR_TURN) {
std::this_thread::sleep_for(std::chrono::milliseconds(MILLI_SECONDS_TO_WAIT));
//std::cout<<"НЕ ВАШЕ ВРЕМЯ ДЛЯ НАПИТКА"<<std::endl;
}
else if(drink_return == DRINK) {
numbDrinksDrunk ++;
//std::cout<<"НАПИТКИ УПЕЧЕНЫ"<<std::endl;
}
else if (drink_return == OUT_OF_DRINKS) {
askForDrinks = false;
//std::cout<<"НЕТ НАПИТКОВ"<<std::endl;
}
}
}
}
.
Ответ или решение
Ошибки в многопоточном программировании, особенно в контексте синхронизации доступа к общим ресурсам, часто могут приводить к непредсказуемым результатам, которые сложно диагностировать. Рассмотрим вашу проблему подробнее:
1. Отсутствие синхронизации в критических секциях
Как вы уже заметили, существует вероятность гонок (race conditions) в вашем коде. Вы упомянули, что все переменные, доступные многими потоками, защищены мьютексами, однако в классе Person
доступ к переменным происходит без синхронизации. Это может вызвать следующие проблемы:
-
Параллельное изменение состояния: Если несколько потоков одновременно обращаются к одной и той же переменной, например,
my_customers
, это может привести к непредсказуемым изменениям данных. Один поток может пытаться добавить нового клиента, пока другой анализирует текущих клиентов, что приводит к несуществующим или устаревшим данным. -
Неправильное распределение супа: Ваш метод
getSoup
проверяет, получили ли другие клиенты суп больше, чем должен, что потребует синхронизации, чтобы избежать того, чтобы один и тот же клиент определялся как следующий в очереди по нескольким потокам.
2. Уровень блокировки
Использование нескольких мьютексов, как soup_mutex
и drink_mutex
, может приводить к ситуации, когда один поток удерживает один мьютекс, а другой поток пытается получить доступ к другому. Это приводит к потенциальному взаимному блокированию (deadlock) и неэффективности.
Рекомендации:
-
Рассмотрите возможность использования одного мьютекса для всех операций, которые модифицируют состояние, вместо отдельных мьютексов для различных ресурсов. Это упростит управление синхронизацией.
-
Являясь более эффективным, вы можете использовать
std::unique_lock
для управления блокировкой мьютекса, позволяя вам более гибко управлять жизненным циклом блокировок.
3. Логика доступа
Логика доступа в методах getSoup
и getDrink
создает ситуации, когда клиент может не получать суп, хотя у него есть право на это. Например, вы сравниваете количество супа у текущего клиента с наименьшим количеством супа, который был выдан:
if (current->numbBowlsSoup > leastBowls) {
std::cout << "NOT YOUR TURN" << std::endl;
return NOT_YOUR_TURN;
}
Такое условие может игнорировать ситуации, когда наливается неравномерно множеством клиентов одновременно. Это требует пересмотра логики выдачи и распределения.
4. Рекомендации для улучшения
-
Обеспечьте синхронизацию: Все операции, связанные с изменением состояния клиентов, должны быть обернуты в блоки мьютексов.
-
Проверка состояния: Убедитесь, что перед каждой проверкой состояния переменные синхронизированы, чтобы обеспечить актуальность данных.
-
Дебаг и логирование: Добавьте больше логов в ключевые точки, чтобы отслеживать порядок выполнения запросов и получать больше информации о состоянии вашей аппроксимации.
-
Тестирование: Регулярно проводите тесты с использованием множества потоков, чтобы имитировать реальную ситуацию с клиентами, и наблюдайте за поведением системы на разных стадиях.
Следуя указанным рекомендациям, вы сможете значительно уменьшить количество неполадок и добиться стабильной работы вашего приложения.