انتقل إلى المحتوى

كائنات المزامنة والأقفال والأقسام الحرجة

أساسيات المزامنة الرئيسية: mutex وlock_guard وUnique_lock وscoped_lock وshared_mutex. تعلم كيفية حماية البيانات المشتركة، ومنع حالات التوقف التام، وفهم الفرق بين سباقات البيانات وظروف السباق.

لماذا يهم المزامنة

عندما تصل عدة سلاسل رسائل إلى نفس الذاكرة ويقوم واحد منها على الأقل بالكتابة، يكون لديك سباق بيانات — وهو شكل من أشكال السلوك غير المحدد في C++. المعيار لا يقول فقط "النتيجة لا يمكن التنبؤ بها". تقول *أي شيء يمكن أن يحدث*: بيانات تالفة، أو أعطال، أو ثغرات أمنية، أو سلوك يبدو صحيحًا والذي ينكسر عند تغيير إشارات المترجم.

mutex (الاستبعاد المتبادل) هو بدائية المزامنة الأساسية. يمكن لخيط واحد فقط أن يحمل قفل كائن المزامنة (mutex) في المرة الواحدة. أي مؤشر ترابط آخر يحاول قفله سوف يحظر** حتى يتم تحرير كائن المزامنة (mutex). من خلال حماية البيانات المشتركة باستخدام كائن المزامنة (mutex)، يمكنك إجراء تسلسل للوصول وإزالة سباقات البيانات.

std::mutex وlock_guard

The simplest and safest pattern isstd::lock_guard— an RAII wrapper that locks a mutex on construction and unlocks it on destruction, even if an exception is thrown.

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 — قفل متعدد خالي من حالة الجمود (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;
    }
};
خطأ شائع

يحدث سباق البيانات عندما يصل خيطان إلى نفس موقع الذاكرة بشكل متزامن ويكتب أحدهما على الأقل، بدون مزامنة. في C++، يعد هذا سلوكًا غير محدد — وليس مجرد "مخرجات غير حتمية" ولكن أي شيء يمكن أن يحدث UB.

حالة السباق هي خطأ منطقي عالي المستوى حيث تعتمد صحة البرنامج على التوقيت. حتى التعليمات البرمجية الخالية من سباقات البيانات يمكن أن تحتوي على شروط سباق (على سبيل المثال، التحقق ثم التصرف على خريطة متزامنة). تقوم كائنات المزامنة بإزالة سباقات البيانات؛ يتطلب القضاء على ظروف السباق تصميمًا دقيقًا لخوارزمياتك المتزامنة.

أفضل ممارسة

استخدم دائمًا أغلفة قفل RAII (lock_guard،scoped_lock،unique_lock) بدلاً من مكالماتmutex::lock()/unlock()الأولية. اجعل الأقسام المهمة قصيرة قدر الإمكان — لا تقم مطلقًا بإجراء عمليات الإدخال/الإخراج أو التخصيص أو حظر المكالمات أثناء الضغط على القفل إذا كان بإمكانك تجنب ذلك. عندما تحتاج إلى العديد من كائنات المزامنة، استخدمstd::scoped_lockلتجنب حالة توقف تام. إذا كان يجب عليك القفل يدويًا، فاحرص دائمًا على القفل بترتيب عالمي متسق.

النقاط الرئيسية
  • سباق البيانات هو سلوك غير محدد — استخدم كائن المزامنة (mutex) لحماية كل متغير مشترك قابل للتغيير
  • std::lock_guardهو أبسط قفل RAII؛ تفضله للأقسام الهامة المباشرة
  • يضيفstd::unique_lockالمرونة: القفل المؤجل، والقفل الموقوت، والقفل/فتح القفل اليدوي
  • يقومstd::scoped_lock(C++17) بتأمين عمليات المزامنة المتعددة في وقت واحد دون حالة توقف تام
  • يسمحstd::shared_mutex(C++17) بقراء متعددين متزامنين ولكن بكتابة حصرية
  • اجعل الأقسام المهمة قصيرة ولا تمسك القفل مطلقًا أثناء انتظار موضوع آخر

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ما هي الميزة الرئيسية لـ`std::scoped_lock`عبر استدعاء`lock()`يدويًا على كائنات المزامنة المتعددة؟

2. ما نوع القفل الذي يجب أن تستخدمه مع`std::condition_variable`؟

3. في C++، ما هي نتيجة سباق البيانات؟