Sobes.tech
Middle+

Está familiarizado com mecanismos de sincronização, como spinlocks e bloqueios mútuos (deadlock)?

sobes.tech IA

Resposta da IA

Sim, conheço.

Spinlock é um mecanismo de sincronização em que um thread que não consegue aceder a um recurso verifica continuamente em ciclo, "girando" (spinning), em vez de passar para um estado de espera (bloqueio). É eficiente para proteger secções críticas que se espera que sejam ocupadas por pouco tempo. Evita os custos de mudança de contexto, mas pode consumir muito tempo de CPU se o bloqueio for mantido durante muito tempo.

Mutex (do inglês "mutually exclusive" - mutuamente exclusivo) é um objeto de sincronização que permite que vários threads partilhem um recurso, mas garante que, num dado momento, apenas um thread utilize o recurso. Se um thread não consegue obter o mutex, fica bloqueado e passa para um estado de espera, libertando a CPU. Os mutex são adequados para proteger secções críticas que podem estar ocupadas por mais tempo.

O deadlock (impasse) é uma situação em que dois ou mais threads esperam indefinidamente que o outro libere recursos. Ocorre quando todas as quatro condições de Coffman são satisfeitas:

  1. Exclusão mútua: Os recursos não podem ser utilizados por vários threads ao mesmo tempo.
  2. Retenção e espera: Os threads mantêm os recursos que já possuem e esperam por outros.
  3. Ausência de libertação forçada: Os recursos não podem ser retirados à força dos threads que os detêm.
  4. Espera cíclica: Existe um ciclo de dois ou mais threads, cada um esperando por um recurso detido pelo próximo no ciclo.

Para evitar deadlocks, podem-se usar estratégias como:

  • Evitar obter múltiplos bloqueios ao mesmo tempo.
  • Definir uma ordem para obter os bloqueios.
  • Utilizar mecanismos de deteção e resolução de deadlocks.

Exemplo de spinlock em C++ usando 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();
    // Secção crítica
    std::cout << "Thread " << std::this_thread::get_id() << " entrou na secção crítica\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Simulação de trabalho
    std::cout << "Thread " << std::this_thread::get_id() << " sai da secção crítica\n";
    spin.unlock();
}

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

Exemplo de mutex em C++ usando std::mutex:

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

std::mutex mtx;
void worker_mutex() {
    mtx.lock();
    // Secção crítica
    std::cout << "Thread " << std::this_thread::get_id() << " entrou na secção crítica\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Simulação de trabalho
    std::cout << "Thread " << std::this_thread::get_id() << " sai da secção crítica\n";
    mtx.unlock();
}

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

Exemplo de possível deadlock:

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

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

// Função que primeiro bloqueia mutex1, depois mutex2
void thread1_func() {
    mutex1.lock();
    std::cout << "Thread 1 bloqueou mutex1\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulação de trabalho
    mutex2.lock(); // Thread 1 espera mutex2, que pode estar ocupado pelo thread 2
    std::cout << "Thread 1 bloqueou mutex2\n";
    // Secção crítica com ambos os mutexes
    mutex2.unlock();
    mutex1.unlock();
}

// Função que primeiro bloqueia mutex2, depois mutex1
void thread2_func() {
    mutex2.lock();
    std::cout << "Thread 2 bloqueou mutex2\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulação de trabalho
    mutex1.lock(); // Thread 2 espera mutex1, que pode estar ocupado pelo thread 1
    std::cout << "Thread 2 bloqueou mutex1\n";
    // Secção crítica com ambos os mutexes
    mutex1.unlock();
    mutex2.unlock();
}

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

Neste exemplo, se o thread 1 bloquear mutex1 e depois dormir, e o thread 2 bloquear mutex2 e também dormir, então o thread 1 aguardará mutex2 (que está ocupado pelo thread 2), e o thread 2 aguardará mutex1 (que está ocupado pelo thread 1). Isto resulta num deadlock.

Para evitar isto, pode-se simplesmente alterar a ordem de aquisição dos mutexes numa das funções, para que seja a mesma em ambas, por exemplo, primeiro mutex1, depois mutex2.