#опытным
Представьте, вы стоите в очереди в поликлинике. Казалось бы вы вот-вот должны зайти в кабинет, но тут перед вами влезают "мне только спросить". После - опять ваша очередь, но приходит следующий абонент с фразой "мне только больничный лист подписать". Вы уже выходите из себя, готовитесь идти напролом в кабинет, но вас прерывает зав отделением, у которого "очень важное дело". Думаю, что жиза для многих.
Итого, вы ждете своей очереди, но всегда появляется кто-то важнее вас, который влезает перед вами. А вы продолжаете ждать. Потенциально до окончания приема и полного обугливания жопы.
Эта сцена наглядно демонстрирует еще одну проблему многопоточного мира - starvation или голодание.
Голодовка в многопоточной передаче происходит, когда один или несколько потоков постоянно блокируются при доступе к ресурсам, в результате чего у них редко бывает возможность выполниться(потенциально никогда). В то время как дедлок замораживает все вовлеченные треды, голодание затрагивает только те невезучие потоки, которые остаются «ожидать в очереди», в то время как другие занимают все ресурсы.
Какие предпосылки появления голодания?
👉🏿 Приоритеты потоков. Хоть в стандарте С++ нельзя выставить приоритет потоков, это можно сделать, например, в pthreads. Потоки с большим приоритетом могут забирать всю работу у низкоприоритетных.
👉🏿 Короткий доступ к мьютексу. Есть два вида замков: справедливые и несправедливые. Поток, только что освободивший unfair мьютекс, имеет преимущество по его захвату, потому что мьютекс все еще может быть в кэше этого потока и у него еще не закончилось время на работу. И это может приводить к простую других потоков. Справедливая реализация учитывает порядок запроса блокировки мьютекса, например с помощью очереди.
👉🏿 Все хотят доступ к одному ресурсу. Когда много потоков пытаются получить доступ к ресурсу, охраняемому всего одним мьютексом, то полезную работу делает только один из них, а все остальные ждут.
👉🏿 Длинные задачи под мьютексом. В дополнение к предыдущему пункту. Мало того, что потоки просто долго ждут очереди, чтобы занять замок, так еще и каждый из них вечность делает свою задачу.
Простой пример:
std::mutex mtx;
int counter = 0;
void worker(int id) {
for (int i = 0; i < 100; ++i) {
std::lock_guard lg{mtx};
++counter;
std::cout << "Thread " << id
<< " entered critical section, counter = " << counter
<< std::endl;
// do work
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::jthread t1(worker, 1);
std::jthread t2(worker, 2);
}
Здесь на первый взгляд все четко, всего два конкурентных потока пытаются залезть в критическую секцию. Вот только незадача: тут конкурентности почти нет. Я конечно не могу говорить за все реализации, но мой личный опыт и годболт подсказывают мне, что практически в каждом прогоне в начале полностью выполнится первый поток, а потом полностью второй.
Но! Если вы добавите слип после релиза мьютекса, то картина становится более справедливой.
Как избавиться от голодания?
✅ Справедливый шедулинг и замки. В стандартных плюсах на это мы не можем повлиять, но в системном апи или самописных реализациях можем.
✅ Минимальный размер критической секции. Она должна менеждить хранение задачи, но не быть ответственной за выполненеие задачи. Это позволит ограничивать простой других потоков.
✅ Грамотно проектируйте разделяемые данные. Если у вас 100 потоков пинают одну несчастную потокобезопасную мапу, то есть высока вероятность пересмотреть архитектуру и межпоточное взаимодействие.
✅ Давайте возможность другим войти в критическую секцию. Учитывая второй пункт, поток, который постоянно стучится в критическую секцию, скорее всего выполняет в ней лишний код. Разгрузите секцию, займите поток чем-нибудь в перерывах между критическими секциями и будет вам счастье.
Remember that you have the highest priority. Stay cool.
#concurrency