Sari la conținut

Mutexuri, încuietori și secțiuni critice

Primitive de sincronizare master: mutex, lock_guard, unique_lock, scoped_lock, shared_mutex. Aflați cum să protejați datele partajate, să preveniți blocajele și să înțelegeți diferența dintre cursele de date și condițiile de cursă.

De ce este importantă sincronizarea

Când mai multe fire accesează aceeași memorie și cel puțin unul dintre ele scrie, aveți o cursă de date — o formă de comportament nedefinit în C++. Standardul nu spune doar „rezultatul este imprevizibil”. Scrie *se poate întâmpla orice*: date corupte, blocări, vulnerabilități de securitate sau comportament aparent corect care se întrerupe atunci când schimbați steagurile compilatorului.

Un mutex (excludere reciprocă) este primitiva fundamentală de sincronizare. Doar un fir poate ține lacătul unui mutex la un moment dat. Orice alt fir care încearcă să-l blocheze va bloca până când mutex-ul este eliberat. Protejând datele partajate cu un mutex, serializați accesul și eliminați cursele de date.

std::mutex și lock_guard

Cel mai simplu și mai sigur model estestd::lock_guard— un înveliș RAII care blochează un mutex la construcție și îl deblochează la distrugere, chiar dacă se aruncă o excepție.

lock_guard_demo.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int shared_counter = 0;

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // RAII lock
        ++shared_counter;  // safe: only one thread at a time
    }  // lock released here automatically
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment, 10000);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Counter: " << shared_counter << "\n";
    // Always prints 100000 — no data race
    return 0;
}

unique_lock — Blocare flexibilă

std::unique_lockeste mai flexibil decâtlock_guard. Acceptă blocarea amânată, blocarea temporizată, blocarea/deblocarea manuală și este cerut destd::condition_variable.

unique_lock.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;

void try_lock_example() {
    // Deferred locking — don't lock yet
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);

    // Try to lock with a timeout
    if (lock.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "Acquired lock\n";
        // ... do work ...
    } else {
        std::cout << "Could not acquire lock within 100ms\n";
    }
    // Automatically unlocks when lock goes out of scope
}

void manual_control() {
    std::unique_lock<std::mutex> lock(mtx);
    // ... critical section ...
    lock.unlock();  // release early if needed
    // ... non-critical work ...
    lock.lock();    // re-acquire
    // ... another critical section ...
}

scoped_lock — Blocare multiplă fără blocare (C++17)

Când trebuie să blocați mai multe mutexuri simultan,std::scoped_lock(C++17) folosește un algoritm intern de evitare a blocajului. Acesta înlocuiește utilizarea manuală predispusă la erori astd::lock.

scoped_lock_bank.cpp
#include <mutex>
#include <thread>
#include <iostream>

struct BankAccount {
    std::mutex mtx;
    double balance = 1000.0;
};

void transfer(BankAccount& from, BankAccount& to, double amount) {
    // Lock both mutexes without risk of deadlock
    std::scoped_lock lock(from.mtx, to.mtx);

    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance += amount;
        std::cout << "Transferred " << amount << "\n";
    }
}  // both mutexes released

int main() {
    BankAccount alice, bob;

    // These two transfers lock accounts in different order,
    // but scoped_lock prevents deadlock.
    std::thread t1(transfer, std::ref(alice), std::ref(bob), 100.0);
    std::thread t2(transfer, std::ref(bob), std::ref(alice), 50.0);

    t1.join();
    t2.join();

    std::cout << "Alice: " << alice.balance
              << ", Bob: " << bob.balance << "\n";
    return 0;
}

shared_mutex — Blocare cititor-scriitor (C++17)

Când citirile depășesc cu mult numărul scrierilor,std::shared_mutexpermite multi cititori simultani, dar acces exclusiv la scriitor. Utilizațistd::shared_lockpentru cititori șistd::unique_lockpentru scriitori.

shared_mutex_cache.cpp
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <iostream>
#include <map>
#include <string>

class ThreadSafeCache {
    mutable std::shared_mutex smtx_;
    std::map<std::string, int> data_;
public:
    int read(const std::string& key) const {
        std::shared_lock lock(smtx_);  // multiple readers OK
        auto it = data_.find(key);
        return it != data_.end() ? it->second : -1;
    }

    void write(const std::string& key, int value) {
        std::unique_lock lock(smtx_);  // exclusive access
        data_[key] = value;
    }
};
Capcană

O cursă de date are loc atunci când două fire accesează aceeași locație de memorie simultan și cel puțin unul scrie, fără sincronizare. În C++, acesta este comportament nedefinit - nu doar „ieșire nedeterministă”, ci orice se poate întâmpla UB.

O condiție de cursă este o eroare logică de nivel superior în care corectitudinea programului depinde de sincronizare. Chiar și cursele fără date pot avea condiții de cursă (de exemplu, verificați-apoi-acționați pe o hartă concomitentă). Mutexurile elimină cursele de date; eliminarea condițiilor de cursă necesită o proiectare atentă a algoritmilor tăi concurenți.

Bună practică

Utilizați întotdeauna pachetele de blocare RAII (lock_guard,scoped_lock,unique_lock) în loc de apeluri brutemutex::lock()/unlock(). Păstrați secțiunile critice cât mai scurte posibil - nu efectuați niciodată I/O, alocări sau blocarea apelurilor în timp ce țineți un lacăt dacă puteți evita acest lucru. Când aveți nevoie de mai multe mutexuri, utilizațistd::scoped_lockpentru a evita blocajele. Dacă trebuie să blocați manual, blocați întotdeauna într-o ordine globală consecventă.

Idei cheie
  • O cursă de date este un comportament nedefinit - utilizați un mutex pentru a proteja fiecare variabilă mutabilă partajată
  • std::lock_guardeste cea mai simplă încuietoare RAII; preferă-l pentru secțiuni critice simple
  • std::unique_lockadaugă flexibilitate: blocare amânată, blocare temporizată și blocare/deblocare manuală
  • std::scoped_lock(C++17) blochează mai multe mutexuri simultan fără blocaj
  • std::shared_mutex(C++17) permite cititori simultani multipli, dar scriitori exclusivi
  • Păstrați secțiunile critice scurte și nu țineți niciodată un lacăt în timp ce așteptați un alt fir

Test — Verifică-ți cunoștințele

(20 XP)

1. Care este principalul avantaj al`std::scoped_lock`față de apelarea manuală`lock()`pe mai multe mutexuri?

2. Ce tip de blocare ar trebui să utilizați cu`std::condition_variable`?

3. În C++, care este consecința unei curse de date?