सामग्री पर जाएं

म्यूटेक्स, लॉक और क्रिटिकल सेक्शन

सिंक्रोनाइज़ेशन प्रिमिटिव में महारत हासिल करें: mutex, lock_guard, unique_lock, scoped_lock, shared_mutex। साझा डेटा की सुरक्षा करना, डेडलॉक रोकना, और डेटा रेस तथा रेस कंडीशन के बीच का अंतर समझना सीखें।

सिंक्रोनाइज़ेशन क्यों ज़रूरी है

जब कई थ्रेड एक ही मेमोरी को एक्सेस करते हैं और उनमें से कम से कम एक लिखता है, तो आपके पास डेटा रेस (data race) होती है — जो C++ में अपरिभाषित व्यवहार (undefined behavior) का एक रूप है। स्टैंडर्ड सिर्फ़ यह नहीं कहता कि "नतीजा अनिश्चित है।" वह कहता है कि *कुछ भी हो सकता है*: करप्ट डेटा, क्रैश, सुरक्षा कमज़ोरियाँ, या ऐसा व्यवहार जो देखने में सही लगे, पर कंपाइलर फ़्लैग बदलते ही टूट जाए।

म्यूटेक्स (mutex) (mutual exclusion) बुनियादी सिंक्रोनाइज़ेशन प्रिमिटिव है। एक समय में केवल एक थ्रेड ही म्यूटेक्स का लॉक पकड़ सकता है। उसे लॉक करने की कोशिश करने वाला कोई भी दूसरा थ्रेड म्यूटेक्स के रिलीज़ होने तक ब्लॉक रहेगा। साझा डेटा को म्यूटेक्स से सुरक्षित करके आप एक्सेस को क्रमबद्ध (serialize) कर देते हैं और डेटा रेस ख़त्म कर देते हैं।

std::mutex और lock_guard

सबसे सरल और सुरक्षित पैटर्न std::lock_guard है — यह एक RAII रैपर है जो कंस्ट्रक्शन पर म्यूटेक्स को लॉक करता है और डिस्ट्रक्शन पर उसे अनलॉक करता है, भले ही एक्सेप्शन फेंका गया हो।

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 से ज़्यादा लचीला है। यह डिफ़र्ड लॉकिंग, टाइम्ड लॉकिंग और मैन्युअल lock/unlock का समर्थन करता है, और 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 — डेडलॉक-मुक्त मल्टी-लॉक (C++17)

जब आपको कई म्यूटेक्स एक साथ लॉक करने हों, तो std::scoped_lock (C++17) अंदर से डेडलॉक-अवॉइडेंस एल्गोरिदम का उपयोग करता है। यह 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 — रीडर-राइटर लॉक (C++17)

जब पढ़ने की संख्या लिखने से बहुत ज़्यादा हो, तो 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) एक उच्च-स्तरीय लॉजिक बग है जिसमें प्रोग्राम की शुद्धता टाइमिंग पर निर्भर करती है। डेटा रेस से मुक्त कोड में भी रेस कंडीशन हो सकती है (जैसे, किसी कंकरेंट मैप पर check-then-act)। म्यूटेक्स डेटा रेस को ख़त्म करते हैं; रेस कंडीशन ख़त्म करने के लिए आपके कंकरेंट एल्गोरिदम का सावधानीपूर्वक डिज़ाइन ज़रूरी है।

सर्वोत्तम अभ्यास

कच्चे mutex::lock()/unlock() कॉल की बजाय हमेशा RAII लॉक रैपर (lock_guard, scoped_lock, unique_lock) का उपयोग करें। क्रिटिकल सेक्शन को जितना हो सके छोटा रखें — अगर इनसे बचा जा सकता हो, तो लॉक पकड़े हुए कभी I/O, एलोकेशन या ब्लॉकिंग कॉल न करें। जब आपको कई म्यूटेक्स चाहिए हों, तो डेडलॉक से बचने के लिए std::scoped_lock का उपयोग करें। अगर आपको मैन्युअल रूप से लॉक करना ही पड़े, तो हमेशा एक सुसंगत ग्लोबल क्रम में लॉक करें।

मुख्य बातें
  • डेटा रेस (data race) अपरिभाषित व्यवहार है — हर साझा म्यूटेबल वेरिएबल की सुरक्षा के लिए म्यूटेक्स का उपयोग करें
  • std::lock_guard सबसे सरल RAII लॉक है; सीधे-सादे क्रिटिकल सेक्शन के लिए इसे प्राथमिकता दें
  • std::unique_lock लचीलापन जोड़ता है: डिफ़र्ड लॉकिंग, टाइम्ड लॉकिंग और मैन्युअल lock/unlock
  • std::scoped_lock (C++17) कई म्यूटेक्स को बिना डेडलॉक के एक साथ लॉक करता है
  • std::shared_mutex (C++17) कई समवर्ती रीडर की अनुमति देता है, लेकिन राइटर को अनन्य एक्सेस
  • क्रिटिकल सेक्शन छोटे रखें और किसी दूसरे थ्रेड का इंतज़ार करते समय कभी लॉक न पकड़ें

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. कई म्यूटेक्स पर मैन्युअली `lock()` कॉल करने की तुलना में `std::scoped_lock` का मुख्य फ़ायदा क्या है?

2. `std::condition_variable` के साथ आपको कौन-सा लॉक प्रकार उपयोग करना चाहिए?

3. C++ में डेटा रेस का क्या परिणाम होता है?