Назад к вопросам
Middle+
88
questionbank

Знакомы ли вы с механизмами синхронизации, такими как спинлоки и взаимные блокировки (deadlock)?

Sobes Copilot

Получайте ответы в реальном времени

Ответ от нейросети

sobes.tech AI

Да, знаком.

Спинлок — это механизм синхронизации, при котором поток, не сумевший получить доступ к ресурсу, постоянно проверяет его доступность в цикле, "крутясь" (spinning), вместо того чтобы перейти в состояние ожидания (блокировки). Эффективен для защиты критических секций, которые, как ожидается, будут заняты очень короткое время. Избегает накладных расходов на переключение контекста, но может потреблять много процессорного времени, если блокировка удерживается долго.

Мьютекс (от англ. mutually exclusive - взаимоисключающий) - это объект синхронизации, который позволяет нескольким потокам совместно использовать один ресурс, но гарантирует, что в любой момент времени ресурс используется только одним потоком. Если поток не может получить мьютекс, он блокируется и переходит в состояние ожидания, освобождая процессор. Мьютексы подходят для защиты критических секций, которые могут быть заняты дольше.

Взаимная блокировка (deadlock) — это ситуация, когда два или более потока навсегда ждут друг друга для освобождения ресурсов. Возникает, когда выполняются все четыре условия Коффмана:

  1. Взаимное исключение: Ресурсы не могут быть использованы несколькими потоками одновременно.
  2. Удержание и ожидание: Потоки удерживают уже выделенные им ресурсы и ждут новых.
  3. Отсутствие принудительного освобождения: Ресурсы не могут быть принудительно отняты у удерживающих их потоков.
  4. Циклическое ожидание: Существует цикл из двух или более потоков, каждый из которых ждет ресурса, занятого следующим потоком в цикле.

Для предотвращения взаимных блокировок можно использовать различные стратегии, например:

  • Избегать получения нескольких блокировок одновременно.
  • Определить порядок получения блокировок.
  • Использовать механизмы обнаружения и устранения взаимных блокировок.

Пример спинлока на C++ с использованием std::atomic_flag:

#include <atomic>
#include <thread>
#include <iostream>

class Spinlock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire));
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

Spinlock spin;
void worker() {
    spin.lock();
    // Критическая секция
    std::cout << "Поток " << std::this_thread::get_id() << " вошел в критическую секцию\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Имитация работы
    std::cout << "Поток " << std::this_thread::get_id() << " выходит из критической секции\n";
    spin.unlock();
}

// int main() {
//     std::thread t1(worker);
//     std::thread t2(worker);
//     t1.join();
//     t2.join();
//     return 0;
// }

Пример мьютекса на C++ с использованием std::mutex:

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
void worker_mutex() {
    mtx.lock();
    // Критическая секция
    std::cout << "Поток " << std::this_thread::get_id() << " вошел в критическую секцию\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Имитация работы
    std::cout << "Поток " << std::this_thread::get_id() << " выходит из критической секции\n";
    mtx.unlock();
}

// int main() {
//     std::thread t1(worker_mutex);
//     std::thread t2(worker_mutex);
//     t1.join();
//     t2.join();
//     return 0;
// }

Пример возможной взаимной блокировки:

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mutex1;
std::mutex mutex2;

// Функция, которая сначала захватывает mutex1, затем mutex2
void thread1_func() {
    mutex1.lock();
    std::cout << "Поток 1 захватил mutex1\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Имитация работы
    mutex2.lock(); // Поток 1 ждет mutex2, который может быть занят потоком 2
    std::cout << "Поток 1 захватил mutex2\n";
    // Критическая секция с обоими мьютексами
    mutex2.unlock();
    mutex1.unlock();
}

// Функция, которая сначала захватывает mutex2, затем mutex1
void thread2_func() {
    mutex2.lock();
    std::cout << "Поток 2 захватил mutex2\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Имитация работы
    mutex1.lock(); // Поток 2 ждет mutex1, который может быть занят потоком 1
    std::cout << "Поток 2 захватил mutex1\n";
    // Критическая секция с обоими мьютексами
    mutex1.unlock();
    mutex2.unlock();
}

// int main() {
//     std::thread t1(thread1_func);
//     std::thread t2(thread2_func);
//     t1.join();
//     t2.join();
//     return 0;
// }

В данном примере, если поток 1 захватит mutex1 и затем уснет, а поток 2 захватит mutex2 и уснет, то затем поток 1 будет ждать mutex2 (который занят потоком 2), а поток 2 будет ждать mutex1 (который занят потоком 1). Возникает взаимная блокировка.

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