رفتن به محتوا

قفل‌های متقابل (Mutex)، قفل‌ها و ناحیه‌های بحرانی

بر ابزارهای هم‌زمان‌سازی تسلط پیدا کنید: mutex، lock_guard، unique_lock، scoped_lock، shared_mutex. محافظت از داده‌های مشترک، جلوگیری از بن‌بست (deadlock) و تفاوت بین مسابقه داده و شرایط رقابتی (race condition) را بیاموزید.

چرا هم‌زمان‌سازی مهم است

وقتی چندین نخ به یک حافظه مشترک دسترسی دارند و حداقل یکی از آن‌ها می‌نویسد، شما یک مسابقه داده (data race) دارید — نوعی رفتار نامعین (undefined behavior) در ++C. استاندارد فقط نمی‌گوید «نتیجه غیرقابل پیش‌بینی است.» بلکه می‌گوید *هر چیزی ممکن است اتفاق بیفتد*: داده‌های خراب، خرابی برنامه، آسیب‌پذیری‌های امنیتی، یا رفتار به‌ظاهر صحیح که وقتی فلگ‌های کامپایلر را تغییر دهید خراب می‌شود.

یک قفل متقابل (mutex) (انحصار متقابل) ابزار اصلی هم‌زمان‌سازی است. فقط یک نخ می‌تواند در هر لحظه قفل یک mutex را نگه دارد. هر نخ دیگری که سعی کند آن را قفل کند، مسدود می‌شود تا mutex آزاد شود. با محافظت از داده‌های مشترک با mutex، دسترسی را سریالیزه کرده و مسابقه‌های داده را حذف می‌کنید.

std::mutex و lock_guard

ساده‌ترین و امن‌ترین الگو std::lock_guard است — یک پوشش RAII که mutex را در هنگام ساخت قفل و در هنگام تخریب باز می‌کند، حتی اگر استثنایی پرتاب شود.

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 — قفل‌گذاری انعطاف‌پذیر

std::unique_lock انعطاف‌پذیرتر از lock_guard است. از قفل‌گذاری تأخیری، قفل‌گذاری زمان‌دار، قفل/بازکردن دستی پشتیبانی می‌کند و برای std::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 — قفل چندگانه بدون بن‌بست (سی‌پلاس‌پلاس ۱۷)

وقتی نیاز به قفل کردن چندین mutex به‌طور همزمان دارید، std::scoped_lock (سی‌پلاس‌پلاس ۱۷) از یک الگوریتم اجتناب از بن‌بست (deadlock) به‌طور داخلی استفاده می‌کند. این جایگزین استفاده دستی خطاپذیر از std::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 — قفل خواننده-نویسنده (سی‌پلاس‌پلاس ۱۷)

وقتی خواندن‌ها به‌مراتب بیشتر از نوشتن‌ها هستند، std::shared_mutex امکان خوانندگان همزمان متعدد اما دسترسی انحصاری نویسنده را فراهم می‌کند. از std::shared_lock برای خوانندگان و std::unique_lock برای نویسندگان استفاده کنید.

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;
    }
};
دام

یک مسابقه داده (data race) زمانی رخ می‌دهد که دو نخ به‌طور همزمان به یک مکان حافظه دسترسی دارند و حداقل یکی می‌نویسد، بدون هم‌زمان‌سازی. در ++C این رفتار نامعین (undefined behavior) است — نه صرفاً «خروجی غیرقطعی» بلکه هر-چیزی-ممکن-است UB.

یک شرایط رقابتی (race condition) یک باگ منطقی سطح بالاتر است که در آن صحت برنامه به زمان‌بندی بستگی دارد. حتی کدی که مسابقه داده ندارد می‌تواند شرایط رقابتی داشته باشد (مثلاً بررسی-سپس-عمل روی یک map همروند). قفل‌های متقابل (mutex) مسابقه‌های داده را حذف می‌کنند؛ حذف شرایط رقابتی نیاز به طراحی دقیق الگوریتم‌های همروند شما دارد.

بهترین شیوه

همیشه از پوشش‌های RAII قفل (lock_guard، scoped_lock، unique_lock) به جای فراخوانی‌های خام mutex::lock()/unlock() استفاده کنید. ناحیه‌های بحرانی (critical section) را تا حد امکان کوتاه نگه دارید — هرگز عملیات ورودی/خروجی، تخصیص حافظه یا فراخوانی‌های مسدودکننده را در حالی که قفل نگه داشته‌اید انجام ندهید اگر می‌توانید از آن اجتناب کنید. وقتی به چندین mutex نیاز دارید، از std::scoped_lock برای جلوگیری از بن‌بست (deadlock) استفاده کنید. اگر مجبور به قفل‌گذاری دستی هستید، همیشه با یک ترتیب سراسری ثابت قفل کنید.

نکات کلیدی
  • یک مسابقه داده (data race) رفتار نامعین است — از mutex برای محافظت از هر متغیر مشترک قابل تغییر استفاده کنید
  • std::lock_guard ساده‌ترین قفل RAII است؛ برای ناحیه‌های بحرانی ساده آن را ترجیح دهید
  • std::unique_lock انعطاف‌پذیری بیشتری فراهم می‌کند: قفل‌گذاری تأخیری، قفل‌گذاری زمان‌دار و قفل/بازکردن دستی
  • std::scoped_lock (سی‌پلاس‌پلاس ۱۷) چندین mutex را به‌طور همزمان بدون بن‌بست قفل می‌کند
  • std::shared_mutex (سی‌پلاس‌پلاس ۱۷) امکان خوانندگان همزمان متعدد اما نویسندگان انحصاری را فراهم می‌کند
  • ناحیه‌های بحرانی را کوتاه نگه دارید و هرگز در حالی که منتظر نخ دیگری هستید قفل نگه ندارید

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. مزیت اصلی `std::scoped_lock` نسبت به فراخوانی دستی `lock()` روی چندین mutex چیست؟

2. با `std::condition_variable` از چه نوع قفلی باید استفاده کنید؟

3. در ++C، نتیجه یک مسابقه داده (data race) چیست؟