C++开发基础之浅谈多线程的锁机制

一、前言

在C++中,多线程的锁机制是确保线程安全的重要组成部分。当多个线程需要访问共享资源时,锁可以防止同时访问,从而避免数据竞争和条件竞争。C++11及其之后的版本提供了多种锁机制,包括互斥锁(mutex)递归锁(recursive mutex)共享锁(shared mutex)读写锁(read-write mutex)

以下是C++中一些常用的锁机制:

  1. 互斥锁(std::mutex)
    • std::mutex是最基本的锁类型,用于保护共享数据,确保同时只有一个线程可以访问该数据。
    • 使用lockunlock方法或者std::lock_guardstd::unique_lock管理类来简化锁的管理。
  2. 递归锁(std::recursive_mutex)
    • std::recursive_mutex允许同一个线程多次锁定同一个互斥量,而不会发生死锁。
    • 主要用于递归函数或者可能递归调用的函数中。
  3. 共享锁(std::shared_mutex)
    • std::shared_mutex是一种读写锁,允许多个线程同时读取共享数据,但写入时需要独占访问。
    • 读取时使用std::shared_lock,写入时使用std::unique_lock
  4. 读写锁(std::read_write_mutex)
    • std::read_write_mutexstd::shared_mutex的旧版本名称,在C++17中已经被废弃。
  5. 条件变量(std::condition_variable)
    • std::condition_variable用于线程间的同步,它允许一个或多个线程等待直到某个条件成立。
    • 通常与std::unique_lock一起使用,用于等待和通知。
  6. 原子操作(std::atomic)
    • std::atomic提供了一种不需要锁就可以保证原子操作的机制,适用于简单的数据类型。
    • 原子操作可以用于实现无锁编程。

二、概念

共享互斥锁(Shared-Mutex)是一种高级的同步机制,它允许多个线程同时读取共享资源,但在写入资源时需要独占访问。这种锁通常用于实现多读单写(read-write)锁,它可以提高程序的性能,尤其是在读操作远多于写操作的情况下。

共享互斥锁的核心概念:

  1. 读锁(共享锁):当线程仅需要读取数据时,它会请求一个读锁。如果有其他线程已经持有读锁,新的读取线程可以立即获得锁,因为读操作不会修改数据,所以多个线程可以同时安全地读取。
  2. 写锁(独占锁):当线程需要修改数据时,它会请求一个写锁。写锁是独占的,这意味着如果有任何线程(无论是读取还是写入)持有锁,写锁请求都必须等待,直到所有其他锁都被释放。
  3. 锁的升级和降级:在某些实现中,锁可以升级(从读锁到写锁)或降级(从写锁到读锁)。这允许线程在必要时获取更高级别的锁以执行写入操作,或者在写入完成后降低锁的级别以允许其他线程读取。
  4. 死锁预防:共享互斥锁的实现通常包括机制来防止死锁,例如,通过定义锁的获取顺序或使用超时机制。

三、使用共享互斥锁std::shared_mutex

1.std::unique_lock

std::unique_lock是C++11引入的,它主要用于独占式访问控制,即在同一时间内只允许一个线程访问资源。它提供了比std::lock_guard更高的灵活性,允许动态地锁定和解锁,以及传递锁的所有权。主要特性如下:

  • 动态锁管理std::unique_lock可以在运行时决定何时锁定和解锁,通过调用lock()unlock()方法。
  • 锁的可转移性:通过移动构造函数和移动赋值运算符,std::unique_lock可以转移锁的所有权,这对于需要在函数间传递锁控制权的场景非常有用。
  • 构造时的灵活性:它可以带有std::defer_lockstd::try_to_lockstd::adopt_lock标记,分别表示不立即锁定、尝试锁定但不阻塞、或采用已锁定的互斥体。
  • 自动解锁:如果std::unique_lock对象离开作用域,它会自动调用unlock()释放锁,遵循RAII(Resource Acquisition Is Initialization)原则。

2.std::shared_lock

std::shared_lock是在C++17中为了配合std::shared_mutex而引入的,主要服务于共享锁的场景,即允许多个线程同时读取共享资源,但任何写入操作需要独占访问。其特性包括:

  • 共享访问:多个std::shared_lock可以同时锁定同一个std::shared_mutex,允许并发读取。
  • 与unique_lock的互斥:当有线程持有std::shared_lock时,任何尝试获取std::unique_lock(写锁)的操作都会被阻塞,直到所有共享锁被释放。
  • 相同的灵活性:与std::unique_lock类似,std::shared_lock也支持构造时的标记(如std::defer_lock),并且在对象生命周期结束时自动解锁。
  • 尝试获取锁:提供了try_lock_shared()方法,允许尝试非阻塞地获取共享锁,失败时立即返回。

std::unique_lock适用于需要独占访问资源的场景,而std::shared_lock则适用于读多写少的场景,可以允许多个读取者同时访问资源,但写入时则排他。两者都通过RAII机制保证了锁的自动管理,减少了忘记解锁带来的风险。

3.示例代码

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

std::shared_mutex sharedMutex;
std::vector<int> data;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(sharedMutex);
    // 当前线程获得了共享锁,可以安全地读取data
    std::cout << "Reader " << id << " is reading data." << std::endl;
    // ...读取data的操作...
}

void writer(int id, int value) {
    std::unique_lock<std::shared_mutex> lock(sharedMutex);
    // 当前线程获得了独占锁,可以安全地修改data
    std::cout << "Writer " << id << " is writing data." << std::endl;
    data.push_back(value);
    // ...写入data的操作...
}

int main() {
    data.reserve(10); // 预分配空间,非必须,仅为示例
    std::thread readers[5], writers[2];

    // 启动读取者线程
    for(int i = 0; i < 5; ++i) {
        readers[i] = std::thread(reader, i+1);
    }

    // 启动写入者线程
    for(int i = 0; i < 2; ++i) {
        writers[i] = std::thread(writer, i+1, i+100);
    }

    // 等待所有线程完成
    for(auto& r : readers) r.join();
    for(auto& w : writers) w.join();

    std::cout << "All operations completed. Data content: ";
    for(const auto& val : data) std::cout << val << " ";
    std::cout << std::endl;

    return 0;
}

在这里插入图片描述

共享访问: 多个线程可以同时持有std::shared_lock,这意味着它们可以同时读取受保护的资源,但不允许任何写入操作同时进行。

  • 提升兼容性: std::shared_lockstd::unique_lock兼容,后者用于独占访问。当一个线程持有了独占锁,其他试图获取共享锁的线程将被阻塞,直到独占锁被释放。
  • 灵活的锁管理: 提供了try_lock方法,允许尝试获取锁而不阻塞,以及unlock方法手动释放锁。
  • 自动管理: 当std::shared_lock对象生命周期结束时(例如离开作用域),会自动释放锁,这有助于防止死锁和资源泄露。

四、使用std::mutex和std::condition_variable互斥锁实现

1.std::mutex

std::mutex 是一个基本的互斥锁,用于保护共享数据,确保同时只有一个线程可以访问该数据。当线程需要访问共享资源时,它必须首先锁定互斥锁,完成操作后再解锁。如果互斥锁已经被其他线程锁定,那么该线程将会阻塞,直到互斥锁被释放。
std::mutex 的主要成员函数包括:

  • lock:锁定互斥锁。如果互斥锁已被其他线程锁定,则当前线程会阻塞。
  • try_lock:尝试锁定互斥锁。如果互斥锁已被其他线程锁定,则不会阻塞,而是返回false。
  • unlock:解锁互斥锁。
    为了简化互斥锁的管理,C++提供了 std::lock_guardstd::unique_lock 这两个辅助类,它们在构造时自动锁定互斥锁,在析构时自动解锁,从而确保互斥锁的正确释放,即使在发生异常时也是如此。

2.std::condition_variable

std::condition_variable 是用于线程间的同步机制,它允许一个或多个线程等待直到某个条件成立。条件变量总是与一个互斥锁一起使用,线程在等待条件变量时会解锁互斥锁,从而允许其他线程访问共享数据。当条件成立时,线程会被唤醒并重新锁定互斥锁。
std::condition_variable 的主要成员函数包括:

  • wait:线程会解锁互斥锁并等待条件变量被唤醒。当线程被唤醒时,它会重新锁定互斥锁。
  • notify_one:唤醒一个等待的线程。
  • notify_all:唤醒所有等待的线程。

3.示例代码

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

class ReadWriteMutex {
public:
    void lock_read() {
        std::unique_lock<std::mutex> lock(mutex_);
        readers_wait_.wait(lock, [this] { return writers_ == 0; });
        ++readers_;
    }

    void unlock_read() {
        std::lock_guard<std::mutex> lock(mutex_);
        --readers_;
        if (readers_ == 0) {
            writers_wait_.notify_all(); // 如果没有读者了,唤醒等待的写者
        }
    }

    void lock_write() {
        std::unique_lock<std::mutex> lock(mutex_);
        writers_wait_.wait(lock, [this] { return readers_ == 0 && writers_ == 0; });
        ++writers_;
    }

    void unlock_write() {
        std::lock_guard<std::mutex> lock(mutex_);
        --writers_;
        if (writers_ == 0) { // 如果没有写者了
            readers_wait_.notify_all(); // 唤醒所有等待的读者
        }
    }

private:
    std::mutex mutex_;
    std::condition_variable readers_wait_, writers_wait_;
    int readers_ = 0;
    int writers_ = 0;
};

// 使用示例
int sharedData = 0;
ReadWriteMutex rwMutex;

void reader(int id) {
    rwMutex.lock_read();
    std::cout << "Reader " << id << " reading data: " << sharedData << std::endl;
    rwMutex.unlock_read();
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟读取时间
}

void writer(int id, int newData) {
    rwMutex.lock_write();
    sharedData = newData;
    std::cout << "Writer " << id << " write data: " << newData << std::endl;
    rwMutex.unlock_write();
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(reader, i + 1);
    }
    threads.emplace_back(writer, 1, 42); // 只有一个写者线程

    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

在这里插入图片描述

这个示例中,ReadWriteMutex类通过维护读取者和写入者的计数,并使用条件变量来同步读写操作,实现了基本的读写锁功能。当没有写者且没有等待的写者时,新的读者可以开始读取;当没有读者且没有其他写者时,写者可以开始写入。

五、使用内核对象互斥体Mutex

在Windows编程中,使用内核对象互斥体(Mutex)来控制多线程的读写操作是一种常见的同步策略。互斥体确保了在某一时刻,最多只有一个线程可以访问共享资源,这对于维护数据一致性至关重要。

1.示例代码

假设我们有一个全局共享资源sharedResource,需要被多个读线程和一个写线程安全地访问。

#include <windows.h>
#include <iostream>
#include <string>
#include <thread>

// 全局共享资源
std::string sharedResource = "Initial Value";
HANDLE g_hMutex; // 用于同步的互斥体句柄

// 读操作函数
void reader(int id) {
    while (true) {
        WaitForSingleObject(g_hMutex, INFINITE); // 等待获取互斥体
        std::cout << "Reader " << id << " is reading: " << sharedResource << std::endl;
        ReleaseMutex(g_hMutex); // 释放互斥体
        Sleep(990); // 模拟读取操作时间,避免过于频繁地尝试获取锁
    }
}

// 写操作函数
void writer(int value) {
    while (true) {
        WaitForSingleObject(g_hMutex, INFINITE); // 等待获取互斥体
        sharedResource = std::to_string(value++);
        std::cout << "Writer wrote: " << sharedResource << std::endl;
        ReleaseMutex(g_hMutex); // 释放互斥体
        Sleep(1000); // 模拟写入操作时间,避免过于频繁地尝试获取锁
    }
}

int main() {
    g_hMutex = CreateMutex(NULL, FALSE, NULL); // 创建互斥体

    std::thread readers[5]; // 创建5个读线程
    for (int i = 0; i < 5; ++i) {
        readers[i] = std::thread(reader, i + 1);
    }

    std::thread writerThread(writer, 1); // 创建1个写线程

    // 等待所有线程完成(实际上这里不会等待,因为线程被设计为无限循环)
    // 实际应用中应根据情况适当结束线程
	while (true)
	{
	}
    return 0;
}

在这里插入图片描述

六、使用SRWLOCK

在Windows API中,可以通过SRWLOCK(共享/独占锁)来实现读写锁的功能。SRWLOCK是一个简单的读写锁,它允许多个线程同时持有读取锁,但只允许一个线程持有写入锁。当有线程持有写入锁时,其他线程无法获取读取锁或写入锁。

1.示例代码:

#include <windows.h>
#include <iostream>
#include <thread>
#include <vector>

SRWLOCK srwLock_;
std::vector<int> data;

void reader(int id) {
    AcquireSRWLockShared(&srwLock_);
    // 当前线程获得了共享锁,可以安全地读取data
    std::cout << "Reader " << id << " is reading data." << std::endl;
    // ...读取data的操作...
    ReleaseSRWLockShared(&srwLock_);
}

void writer(int id, int value) {
    AcquireSRWLockExclusive(&srwLock_);
    // 当前线程获得了独占锁,可以安全地修改data
    std::cout << "Writer " << id << " is writing data." << std::endl;
    data.push_back(value);
    // ...写入data的操作...
    ReleaseSRWLockExclusive(&srwLock_);
}

int main() {
    data.reserve(10); // 预分配空间,非必须,仅为示例
    std::thread readers[5], writers[2];

    InitializeSRWLock(&srwLock_);
    // 启动读取者线程
    for (int i = 0; i < 5; ++i) {
        readers[i] = std::thread(reader, i + 1);
    }

    // 启动写入者线程
    for (int i = 0; i < 2; ++i) {
        writers[i] = std::thread(writer, i + 1, i + 100);
    }

    // 等待所有线程完成
    for (auto& r : readers) r.join();
    for (auto& w : writers) w.join();

    std::cout << "All operations completed. Data content: ";
    for (const auto& val : data) std::cout << val << " ";
    std::cout << std::endl;

    return 0;
}

七、最佳实践建议

在使用C++进行多线程编程时,锁机制是保证数据一致性和避免竞态条件的关键。然而,如果不正确地使用锁,可能会导致死锁、性能问题或者难以发现的bug。

  1. 避免死锁
    • 确保锁的获取顺序一致,避免在不同的线程中以不同的顺序获取多个锁。
    • 使用锁层次结构,避免循环依赖。
  2. 最小化锁的范围
    • 锁定区域(临界区)应该尽可能小,只包含必要的代码。
    • 在进入临界区之前获取锁,在离开临界区之后释放锁。
  3. 不要在锁内持有阻塞操作
    • 避免在持有锁的情况下进行IO操作、网络通信或者等待其他资源,这可能会导致整个程序挂起。
  4. 避免锁竞争
    • 设计良好的数据结构和算法,减少对共享资源的访问。
    • 使用锁分解技术,将一个锁分解为多个锁,减少锁的竞争。
  5. 锁的性能影响
    • 锁会引入开销,影响程序的性能。在某些情况下,可以考虑使用无锁编程或者原子操作来减少锁的开销。
    • 使用读写锁(如std::shared_mutex)而不是互斥锁,允许多个读者同时访问数据,提高并发性能。
  6. 锁的粒度
    • 选择合适的锁粒度,过粗的锁会导致性能问题,过细的锁会增加编程复杂性。
  7. 异常安全
    • 使用std::lock_guardstd::unique_lock等锁管理类,它们可以保证在异常情况下自动释放锁。
    • 避免在锁的保护下抛出异常,或者在抛出异常之前释放锁。
  8. 锁的保护对象
    • 确保锁保护的是正确的数据结构或资源,避免锁保护的范围过大或过小。
  9. 避免锁的滥用
    • 不要对每个函数都使用锁,只对需要保护共享资源的代码使用锁。
    • 在单线程环境中或者没有共享资源的情况下,不需要使用锁。
  10. 锁的测试和调试
    • 对多线程程序进行充分的测试,包括压力测试和竞态条件测试。
    • 使用工具如线程检查器或者锁调试器来帮助检测锁的问题。
      遵循这些注意事项可以帮助你编写更加健壮和高效的多线程程序。在实际开发中,多线程编程往往需要仔细的设计和测试,以确保程序的正确性和性能。
  • 31
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++多线程编程中,锁是一种机制,用于控制对共享资源的访问。当多个线程同时读写同一个共享资源时,就会发生竞争条件(race condition)。为了避免这种情况,我们需要使用锁来确保每个线程都能按照预期方式访问共享资源。 在C++中,常用的锁包括互斥锁(mutex)、读写锁(read-write lock)和条件变量(condition variable)。 互斥锁是最基本的锁,用于保护对共享资源的互斥访问。当一个线程获得了互斥锁后,其他线程就无法访问共享资源,直到该线程释放锁。互斥锁的使用方法如下: ```c++ #include <mutex> std::mutex m; void my_function() { std::lock_guard<std::mutex> lock(m); // 访问共享资源的代码 } ``` 这里使用了std::lock_guard类,它是一个RAII(资源获取即初始化)对象,用于自动获取和释放锁。在my_function函数中,当std::lock_guard对象被创建时,它会自动获取互斥锁;当my_function函数返回时,std::lock_guard对象会自动释放锁。 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。当一个线程获得了写锁时,其他任何线程都无法访问共享资源。当一个线程获得了读锁时,其他线程也可以获得读锁,但不能获得写锁。使用读写锁的方法如下: ```c++ #include <shared_mutex> std::shared_mutex m; void read_function() { std::shared_lock<std::shared_mutex> lock(m); // 读取共享资源的代码 } void write_function() { std::unique_lock<std::shared_mutex> lock(m); // 写入共享资源的代码 } ``` 这里使用了std::shared_lock类和std::unique_lock类,它们分别用于获取读锁和写锁。当std::shared_lock对象被创建时,它会自动获取读锁;当std::unique_lock对象被创建时,它会自动获取写锁。与互斥锁一样,这些锁也是RAII对象,会自动释放锁。 条件变量用于在线程之间传递信号,以便某个线程可以等待另一个线程发出的信号。条件变量通常与互斥锁一起使用。使用条件变量的方法如下: ```c++ #include <condition_variable> std::mutex m; std::condition_variable cv; void my_function() { std::unique_lock<std::mutex> lock(m); // 等待信号的代码 cv.wait(lock); // 收到信号后的代码 } void send_signal() { cv.notify_one(); } ``` 这里使用了std::condition_variable类,它用于等待信号和发送信号。在my_function函数中,当cv.wait(lock)被调用时,当前线程会被阻塞,直到收到信号;在send_signal函数中,cv.notify_one()用于发送一个信号,唤醒一个等待线程。与互斥锁和读写锁一样,std::unique_lock也是RAII对象,会自动释放锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dotnet研习社

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值