View in Telegram
Дедлокаем код #новичкам Частый вопрос на собесах или даже на скринингах - сколько минимум нужно мьютексов, чтобы гарантировано скрафтить дедлок. Прежде, чем приступим к разбору, небольшое введение. Deadlock - это ситуация, когда два или более потока оказываются заблокированными и не могут продолжить свою работу, так как каждый из них ожидает ресурс, который удерживает другой заблокированный поток. В результате, ни один из потоков не может завершиться, а система оказывается в застойном состоянии. Многопоточка - это мир магии и чудес. Ошибки, которые появляются по причине плохой организации доступа потоков к ресурсам - самые трудновоспроизводимые и сложные для отладки. И никогда не знаешь откуда выстрелит. Да и сами ошибки принимают причудливые облики. Поэтому дедлоки могут по разному быть проявлены в коде, но поведение программы всегда при этом стабильно убогое - потоки не могут дальше продолжать производить работу. В рамках текущего поста будем обсуждать так называемую циклическую блокировку. Ща поймете, о чем речь. Стандартный ответ на вопрос из начала - 2. В одном потоке блочим в начале первый мьютекс, потом второй, а в другом потоке наоборот. Таким образом может произойти ситуация, когда оба потока захватили в заложники каждый по одному мьютексу и бесконечно висят в ожидании освобождения второго. Этого никогда не произойдет, потому что каждому нужен замок, который уже захватил другой поток и он его отдавать не собирается, пока пройдет всю критическую секцию. А он ее никогда не пройдет. В общем, собака пытается укусить себя за чресла. Самый простой пример, который иллюстирует эту ситуацию:
std::mutex m1;
std::mutex m2;

std::thread t1([&m1, &m2] {
  std::cout << "Thread 1. Acquiring m1." << std::endl;
  m1.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "Thread 1. Acquiring m2." << std::endl;
  m2.lock();
  std::cout << "Thread 1 perform some work" << std::endl;
});

std::thread t2([&m1, &m2] {
  std::cout << "Thread 2. Acquiring m2." << std::endl;
  m2.lock();
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
  std::cout << "Thread 2. Acquiring m1." << std::endl;
  m1.lock();
  std::cout << "Thread 2 perform some work" << std::endl;
});

t1.join();
t2.join();
Все канонично. В двух разных потоках пытаемся залочить замки в разном порядке. В итоге вывод будет такой:
Thread 1. Acquiring m1.
Thread 2. Acquiring m2.
Thread 2. Acquiring m1.
Thread 1. Acquiring m2.
И дальше программа будет бесконечно простаивать без дела, как тостер на вашей кухне. Do useful work. Stay cool. #concurrency
Love Center
Love Center
Find friends or serious relationships easily