CodeNebula
C++17stladvanced

std::scoped_lock

C++17 · Practical Modern · Compiler flag: -std=c++17

Explanation

Locks one or more mutexes simultaneously using a deadlock-avoidance algorithm, and releases them all when destroyed. It is a variadic improvement over `std::lock_guard` that eliminates the need to manually call `std::lock` followed by adopting lock guards.

Real-World Scenario

Transfer funds between two bank accounts, locking both account mutexes without risk of deadlock.

Quick Snippet

snippet.cpp
#include <mutex>

struct Account {
    std::mutex mtx;
    double balance = 0.0;
};

void transfer(Account& from, Account& to, double amount) {
    std::scoped_lock lock(from.mtx, to.mtx);  // deadlock-free
    from.balance -= amount;
    to.balance += amount;
}

Full Example

scoped_lock.cpp
// CodeNebula - C++17 Example: std::scoped_lock
// Compile: g++ -std=c++17 -Wall -Wextra -o example scoped_lock.cpp -lpthread

// Demonstrates std::scoped_lock, a C++17 RAII lock guard that can lock
// multiple mutexes simultaneously without deadlock (using a deadlock-avoidance
// algorithm). It replaces the error-prone pattern of manually calling
// std::lock() followed by std::lock_guard with std::adopt_lock.

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <string>

// ------------------------------------------------------------------
// Bank account with mutex for thread-safe operations
// ------------------------------------------------------------------
struct Account {
    std::string name;
    double balance;
    std::mutex mutex;

    Account(std::string n, double b) : name(std::move(n)), balance(b) {}
};

// Transfer money between accounts: locks BOTH mutexes to avoid deadlock
void transfer(Account& from, Account& to, double amount) {
    // scoped_lock acquires both mutexes atomically, preventing deadlock
    // even if another thread calls transfer(to, from, ...) simultaneously
    std::scoped_lock lock(from.mutex, to.mutex);

    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance += amount;
    }
}

// ------------------------------------------------------------------
// Logging system with multiple protection levels
// ------------------------------------------------------------------
class Logger {
    std::mutex data_mutex_;
    std::mutex format_mutex_;
    std::vector<std::string> logs_;
    std::string prefix_ = "[LOG] ";

public:
    // Needs both mutexes to safely format and store
    void log(const std::string& msg) {
        std::scoped_lock lock(data_mutex_, format_mutex_);
        logs_.push_back(prefix_ + msg);
    }

    void set_prefix(const std::string& p) {
        std::scoped_lock lock(format_mutex_);
        prefix_ = p;
    }

    void print_all() {
        std::scoped_lock lock(data_mutex_);
        for (const auto& entry : logs_) {
            std::cout << "  " << entry << std::endl;
        }
    }
};

int main()
{
    // ------------------------------------------------------------------
    // 1. Single mutex (replaces lock_guard)
    // ------------------------------------------------------------------
    std::cout << "=== Single Mutex ===" << std::endl;
    std::mutex m;
    {
        std::scoped_lock lock(m);  // Locks one mutex, just like lock_guard
        std::cout << "  Locked single mutex" << std::endl;
    }
    std::cout << "  Unlocked on scope exit" << std::endl;

    // ------------------------------------------------------------------
    // 2. Deadlock-free bank transfers
    // ------------------------------------------------------------------
    std::cout << "\n=== Bank Transfers ===" << std::endl;
    Account alice("Alice", 1000.0);
    Account bob("Bob", 1000.0);

    std::cout << "  Before: Alice=" << alice.balance << " Bob=" << bob.balance << std::endl;

    std::vector<std::thread> threads;
    // Multiple threads transferring in both directions simultaneously
    for (int i = 0; i < 100; ++i) {
        threads.emplace_back(transfer, std::ref(alice), std::ref(bob), 10.0);
        threads.emplace_back(transfer, std::ref(bob), std::ref(alice), 10.0);
    }
    for (auto& t : threads) t.join();

    std::cout << "  After:  Alice=" << alice.balance << " Bob=" << bob.balance << std::endl;
    std::cout << "  Total:  " << (alice.balance + bob.balance)
              << " (should be 2000)" << std::endl;

    // ------------------------------------------------------------------
    // 3. Multi-resource logger
    // ------------------------------------------------------------------
    std::cout << "\n=== Multi-Resource Logger ===" << std::endl;
    Logger logger;

    std::vector<std::thread> log_threads;
    for (int i = 0; i < 5; ++i) {
        log_threads.emplace_back([&logger, i]() {
            logger.log("message from thread " + std::to_string(i));
        });
    }
    for (auto& t : log_threads) t.join();
    logger.print_all();

    // ------------------------------------------------------------------
    // 4. Zero mutexes (no-op, but valid)
    // ------------------------------------------------------------------
    std::cout << "\n=== Zero Mutexes ===" << std::endl;
    {
        std::scoped_lock lock;  // Locks nothing -- valid and useful in templates
        std::cout << "  scoped_lock with no mutexes: valid no-op" << std::endl;
    }

    return 0;
}

/*
Expected output (thread message order may vary):

=== Single Mutex ===
  Locked single mutex
  Unlocked on scope exit

=== Bank Transfers ===
  Before: Alice=1000 Bob=1000
  After:  Alice=1000 Bob=1000
  Total:  2000 (should be 2000)

=== Multi-Resource Logger ===
  [LOG] message from thread 0
  [LOG] message from thread 1
  [LOG] message from thread 2
  [LOG] message from thread 3
  [LOG] message from thread 4

=== Zero Mutexes ===
  scoped_lock with no mutexes: valid no-op
*/
Run command
g++ -std=c++17 -Wall -Wextra -o example scoped_lock.cpp -lpthread