浅谈锁机制(C++、Qt)
本文最后更新于 121 天前,其中的信息可能已经有所发展或是发生改变。

1. 锁机制概述

1.1 概念与作用

在程序使用多线程处理事务时,难免会遇到访问共享资源的情况,而为了共享资源能够被合理地分配与使用,就引入了锁机制来保证线程访问共享资源的互斥性同步性,最终目的就是为了保证多线程不会因竞争共享资源而导致死锁等问题,合理地使用锁机制能保证程序的有效运行。

  • 互斥性:在同一时刻,不能有多个线程访问同一共享资源,即让共享资源在一个时刻只能被一个线程访问。
  • 同步性:让多个线程有序地来访问同一共享资源,严格依赖规定先后访问,一个线程的运行依赖另一个线程访问资源后的资源。

死锁:多个线程因争夺一片共享资源而造成互相限制的一种状况,每个线程既无法获得更多需要的资源,也无法释放已有的资源,互相等待资源而无法进一步执行。

产生死锁的四个必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

(更多内容自行参考《操作系统》等资料)

1.2 悲观锁与乐观锁

悲观锁与乐观锁是使用锁机制的两种情景方式

  • 悲观锁:总是假设最坏的情况,每次线程访问资源的时候都认为会进行修改等不可共享的操作,所以对于每次资源被访问前都会上锁,这样就每次只能有一个线程访问,阻塞其他需求的线程。很多传统锁机制都用的悲观锁机制,比如行锁,表锁等,读锁,写锁等。
  • 乐观锁:总是假设最好的情况,每次线程访问资源的时候都认为不会进行修改等不可共享的操作,所以对于每次资源被访问前都不会上锁,但是在进行不可共享操作的时候会判断一下在此期间是否有其他线程以不可共享的操作访问这片资源。乐观锁适用于多读的应用类型,这样可以提高吞吐量,可以使用版本号机制和CAS算法(见第3节)实现。

1.3 注意事项

  • 锁机制是对于多线程使用的,对于单一线程使用无意义。
  • 锁的性能再很多时候都不高,在底层上避免使用锁是提升性能的关键。
  • 锁粒度过大会降低并发性,尽量将大锁拆分为小锁,尽量缩小锁的作用范围,只保护真正需要保护的关键代码段,也能提高性能。
  • 避免长时间持锁,会降低系统的吞吐量。
  • 锁机制的种类很多,尽量区分情况使用,不要为方便使用通用的锁。比如多读少写的情况就使用读写锁,一昧使用互斥锁会降低性能。
  • 第2节是对C++中各种锁机制的初步使用介绍,每一种锁机制都有更深入的用法,在需要的时候再针对性学习对应方法。

2. C++中的锁

注意:一下是对C++中各种锁机制的初步使用介绍,每一种锁机制都有更深入的用法,在需要的时候再针对性学习对应方法。

2.1 互斥锁(std::mutex

  1. 定义: 最基本的锁,在任意时刻,只有一个线程能获取该锁,当一个线程获取互斥锁后,其他试图获取该锁的线程都会被阻塞。

  2. 使用场景: 一个线程t1占用资源时加锁,其他线程如t2同时访问资源会因无法获得锁被阻塞,直到能获取到锁。

  3. 代码:

    先说明一个C++11中新增加的自动锁销的方法:std::lock_guard。使用此方法可以在对象生命周期结束时自动解锁并释放资源,即自动加锁,再在最后自动解锁并销毁。

    #include 
    std::mutex mtx;
    
    /// 原始方法
    mtx.lock();
    // ...
    mtx.unlock();
    
    /// 自动化方法
    std::lock_guard lock(mtx);
    // ...

    互斥锁示例如下。

    #include 
    #include 
    #include 
    
    std::mutex mtx;
    int count = 0;
    
    void countAndPrint(std::string str){
       std::lock_guard lock(mtx); // 自动锁销
       int countNum = 0;
       do{
           std::cout << str << ":" << count << std::endl;
           count++; 
           countNum++; // 用于计数,累加十次
       }while(countNum < 10);
    }
    
    int main() {
       std::thread t1(countAndPrint, "Thread 1");
       std::thread t2(countAndPrint, "Thread 2");
    // t1先执行完十次累加再由t2继续执行,t1执行期间t2阻塞
    
       t1.join();
       t2.join();
    
       return 0;
    }

2.2 递归互斥锁(std::recursive_mutex

  1. 定义: 允许同一个线程重复获取同一互斥锁,其内部会自动记录锁被获取的次数,每次获取此锁都会计数加1,释放锁时再减1,直到计数为0时才真正释放锁,此时才允许别的线程获取锁,否则阻塞其他线程。

  2. 使用场景: 一个线程t1会递归调用函数,如果使用普通锁,就只能在最外层函数中获取到锁,而阻塞之后的函数调用,而使用递归互斥锁就能为此线程一直获取锁。也适用于函数A调用函数B,函数B又会调用A等复杂递归情况。

  3. 代码:

    #include 
    #include 
    #include 
    
    std::recursive_mutex rec_mtx;
    
    void recursiveFunc(int count){
       if (count <= 0) return;
       std::lock_guard lock(rec_mtx); // 自动锁销
       std::cout << "Count: " << count << std::endl;
       recursive_function(count - 1); // 在一个线程内递归调用
    }
    
    int main(){
       std::thread t1(recursiveFunc, 5);
       t1.join();
       return 0;
    }

2.3 定时互斥锁(std::timed_mutex

  1. 定义: 在原本互斥锁的基础上增加了对时间的判断,允许在设定时间内试图获取锁,如果在计时结束前没有获取锁,则会返回失败,根据返回值可以指定运行的代码,以实现执行获取锁失败后的其他操作。

  2. 使用场景: 线程只会在限定时间内试图获取锁,线程不会一直等待,超时后就放弃获取锁转,而执行其他操作。使用定时互斥锁就不用再手动监控等待时间来释放线程。

  3. 代码:

    注意使用try_lock_for()来寻求加锁,参数为等待获取锁的限定时间。

    #include 
    #include 
    #include 
    #include 
    
    std::timed_mutex tmtx;
    
    void attemptLock(){
       if(tmtx.try_lock_for(std::chrono::seconds(2))){ // 只会在2秒内持续等待锁
           std::cout << "Lock acquired by thread." << std::endl;
           std::this_thread::sleep_for(std::chrono::seconds(3));
           tmtx.unlock();
       }else{ // 超时未获取到锁后执行其他操作,也可以直接退出线程
           std::cout << "Failed to acquire lock." << std::endl;
       }
    }
    
    int main(){
       std::thread t1(attemptLock);
       std::thread t2(attemptLock);
    
       t1.join();
       t2.join();
       return 0;
    }

2.4 递归定时互斥锁(std::recursive_timed_mutex

  1. 定义: 具有递应锁和定时锁的特性,可以重复获取,且允许试图在限时内获取。
  2. 使用场景: 与递应互斥锁和定时互斥锁相同,应用于一个线程既需要递归调用又需要限制时间的情况。
  3. 代码:联想递归锁与定时锁同时使用的情况,略。

2.5 读写锁(std::shared_mutex

  1. 定义: (原方法名为std::read_write_mutex,在C++17中被替换成std::shared_mutex)允许多个线程同时进行读操作,但仅允许一个线程进行写操作。

    此方法区分对共享资源的读操作和写操作,分成两种获取模式:共享模式(std::shared_lock)(读模式)独占模式(std::unique_lock)(写模式)

    多个线程可以同时以共享模式获取读写锁,即同时读,而只能在一个时刻只有一个线程以独占模式获取读写锁,即互斥写。

  2. 使用场景: 可以应对更为复杂的场景,将读写操作分开处理,更适用于处理读多而写少的场景,以提高性能。

  3. 代码:

    两种模式的使用如下,可以直接使用自动锁销的方式。

    #include 
    std::shared_mutex sh_mtx;
    /// 共享模式
    std::shared_lock lock(sh_mtx);
    /// 独占模式
    std::unique_lock lock(sh_mtx);
    #include 
    #include 
    #include 
    
    std::shared_mutex sh_mtx;
    int shared_data = 0;
    
    void read() {
       std::shared_lock lock(sh_mtx);
       std::cout << "Read data: " << shared_data << std::endl;
    }
    
    void write(int value) {
       std::unique_lock lock(sh_mtx);
       shared_data = value;
       std::cout << "Wrote data: " << shared_data << std::endl;
    }
    
    int main() {
       std::vector readThreads;
    for(int i = 0; i < 5; i++){ // 同时读不会阻塞
        readThreads.push_back(std::thread(read));
    }
    std::thread writeThread(write, 10); // 只能独占写,同时写会阻塞其他线程
    
    for(auto& t : readThreads){
        t.join();
    }
    write_thread.join();
       return 0;
    }

2.6 原子操作(std::atomic

  1. 定义: 提供元子级判断和操作,无需键位锁。

  2. 使用场景: 充分利用多核,完全避免互斥锁。

  3. 代码:

    std::atomic针对不同情况都有着不同使用,这里只对部分进行说明

    原子操作默认支持的变量类型为int, bool, char, float, double ,而通过std::atomic<T>也支持使用其他的自定义变量类型T,但也有着特定要求。

    其支持多种原子操作,基本上有:

    • 加载 (load)
    • 存储 (store)
    • 交换 (exchange)
    • 比较并交换 (compare_exchange_strong, compare_exchange_weak)
    • 增量和减量 (fetch_add, fetch_sub)

    原子操作对调用执行的顺序也有着设置,支持通过传入memory_order来设置内存顺序,常见的有:

    • memory_order_relaxed:不保证任何顺序,仅保证原子性。适合不依赖顺序的场景
    • memory_order_acquire:保证在获取数据后,之前的所有写操作对当前线程可见。
    • memory_order_release:保证在释放数据前,当前线程的写操作对其他线程可见。
    • memory_order_acq_rel:结合了 acquire 和 release 的效果。
    • memory_order_seq_cst:默认行为,顺序一致性。

    基础使用示例如下(简单介绍一下fetch_add和compare_exchange_strong的使用情况):

    #include 
    #include 
    #include 
    
    std::atomic counter = 0; // 声明了一个初始值为0的原子int类型变量
    
    void increment() {
       for(int i = 0; i < 10; ++i){
           // 使用fetch_add来增加,第二个参数指定内存顺序
           counter.fetch_add(1, std::memory_order_relaxed); 
       }
    }
    
    void compareAndSwap(int expected, int desired){
       // 使用compare_exchange_strong尝试将counter的值从expected修改为desired。counter等于expected,修改成功,返回true;否则修改失败,返回false。 
       // 注:compare_exchange_strong是强比较交换,失败率较低。compare_exchange_weak是弱比较交换,适合用于循环中。
       if(counter.compare_exchange_strong(expected, desired)){
           std::cout << "Swap successful! New value: " << value << std::endl;
       }else{
           std::cout << "Swap failed. Expected: " << expected << ", actual: " << value << std::endl;
       }
    }
    
    int main() {
       std::thread t1(increment);
       std::thread t2(increment);
       t1.join();
       t2.join();
    
       std::thread t3(compareAndSwap, 10, 20);
       std::thread t4(compareAndSwap, 10, 30);
       t3.join();
       t4.join();
    
       return 0;
    }

2.7 自旋锁(std::atomic_flag

  1. 定义: 一种基于原子操作实现的忙等待的锁机制,当一个线程尝试获取自旋锁而锁已经被占用时,这个线程不会进入阻塞状态,而是会不断地检查(“自旋”)锁是否已经被释放。

  2. 使用场景: 自旋锁在等待时间较短的情况下可能会有比较好的性能表现,因为它避免了线程切换的开销。但是,如果等待时间过长,由于线程一直在占用 CPU 资源进行检查,会导致 CPU 资源的浪费。一般在底层代码或者对性能要求极高、等待时间预计很短的场景下使用。

  3. 代码:

    注意定义锁、等待加锁、释放锁的操作格式。

    #include 
    #include 
    #include 
    
    std::atomic_flag lockFlag = ATOMIC_FLAG_INIT;
    
    void spin_lock_example(int id){
       while(lockFlag.test_and_set(std::memory_order_acquire)); // 等待(自旋)锁
       std::cout << "Thread " << id << " acquired lock." << std::endl;
       lock_flag.clear(std::memory_order_release); // 释放锁
    }
    
    int main(){
       std::thread t1(spin_lock_example, 1);
       std::thread t2(spin_lock_example, 2);
    
       t1.join();
       t2.join();
       return 0;
    }

2.8 条件变量(std::condition_variable

  1. 定义: 条件变量本身不是一种锁,但其常与互斥锁一起使用,实现线程间的同步。条件变量允许一个线程等待一个条件,当另外的线程触发该条件后会告知在等待此条件的线程让其继续进行。

  2. 使用场景: 用于线程之间的协同,比如对于一片共享资源,每个线程访问前有其他条件(或者需要其他线程提供的资源),所以不会直接获取锁,直到满足条件并且得到可以获取锁的通知时再获取锁。

  3. 代码:

    一般使用的格式:

    std::mutex mtx; // 互斥锁与条件变量配合使用
    std::condition_variable cv;
    
    /// 资源需求方,即需要等待资源
    std::unique_lock lock(mtx);
    cv.wait(lock); // 等待时指定锁,在等待时先释放此锁,当得到通知可以获取时再获取锁
    
    /// 资源生产方,即为资源需求方提供需要的资源
    std::unique_lock lock(mtx); // 先加锁
    cv.notify_all(); // 结束时通知所有等待的线程,同时此线程释放锁以便其他线程获取锁

    wait()方法支持使用第二参数“条件谓词”来添加等待条件,通常是返回值为bool类型的函数,或者Lambda表达式也行,它用于检查某个条件是否满足,在返回true是才会唤醒,如下完整示例。

    #include 
    #include 
    #include 
    #include 
    
    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;
    
    void print_id(int id) {
       std::unique_lock lock(mtx);
       cv.wait(lock, []{return ready;}); // 不止需要接收到通知,同时还需要满足ready为true
       std::cout << "Thread " << id << " is ready." << std::endl;
    }
    
    void set_ready() {
       std::unique_lock lock(mtx);
       ready = true; // 置ready为true
       cv.notify_all(); // 通知等待的线程可以获取锁了
    }
    
    int main() {
       std::thread t1(print_id, 1);
       std::thread t2(print_id, 2);
    
       std::this_thread::sleep_for(std::chrono::seconds(1));
       set_ready();
    
       t1.join();
       t2.join();
       return 0;
    }

3. CAS算法

3.1 概念

CAS(比较与交换,Compare-And-Swap)是一种用于原子操作的算法,它是实现无锁编程和并发控制的重要工具。CAS算法的核心是比较内存中的值与给定的预期值,如果它们相等,则将内存中的值更新为新的值,否则不做任何操作。这种操作是原子的,即在执行期间不会被其他线程中断,确保了并发环境下的安全性。

CAS 算法常用于实现一些无锁数据结构,如无锁链表、无锁队列等。

3.2 实现

通常会与原子操作配合实现,如下与std::atomic中的compare_exchange_weak()compare_exchange_strong()配合实现。

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter(0);  // 定义一个原子计数器

void increment(int id){
    int expected = counter.load();  // 获取当前值
    int desired = expected + 1;     // 新的期望值

    // 使用 CAS 操作来原子地增加计数器
    while(!counter.compare_exchange_weak(expected, desired)){
        // 如果当前值与期望值不相等,更新期望值为当前值并重新尝试
        desired = expected + 1;
    }
    std::cout << "Thread " << id << " incremented counter to " << desired << std::endl;
}

int main(){
    const int numThreads = 10;
    std::vector<std::thread> threads;

    // 启动10个线程进行计数器的递增操作
    for(int i = 0; i < numThreads; ++i){
        threads.push_back(std::thread(increment, i));
    }
    for(auto& t : threads){
        t.join();
    }

    std::cout << "Final counter value: " << counter.load() << std::endl;
    return 0;
}

4. Qt中的锁

4.1 互斥锁(QMutex

最简单的互斥锁机制,也有指定方法实现加锁和解锁,而且QMutex中即可实现递归互斥锁和定时互斥锁。

  1. 声明

    直接使用QMutex类的构造函数声明,可以在声明的时候通过递归模式的枚举值QMutex::RecursionMode指定是否使用递归模式,枚举值包含:

    • QMutex::Recursive:递归模式。一个线程可以多次获取同一个互斥锁,需要在所有锁被释放后才会彻底释放此锁。
    • QMutex::NonRecursive:非递归模式,默认值。一个线程只能获取一个互斥锁,不适用于有递归的线程。

    示例:

    QMutex mtx(QMutex::Recursive);
  2. 常用方法

    • void lock():直接获取互斥锁。

    • void unlock():释放互斥锁。

    • bool tryLock(int timeout = 0):尝试获取锁,参数设置时间限制,单位为毫秒。如果设置参数设置为负数就相当于lock()

      注:这个方法更好用,还有同功能的bool try_lock()bool try_lock_for(持续时间),这两个方法是为了与标准库std兼容,一般不用。

    • bool isRecursive():判断是否使用的递归模式

  3. 代码示例

    #include 
    #include 
    #include 
    
    QMutex mtx;
    
    void threadFunction(){
       mtx.lock(); 
       std::cout << "Thread is running!" << std::endl;
       mtx.unlock(); 
    }
    
    int main(){
       QThread t1, t2;
    
       t1.run(&threadFunction);
       t2.run(&threadFunction);
    
       t1.wait();
       t2.wait();
    
       return 0;
    }

4.2 局部互斥锁(QMutexLocker

QMutexLocker不是一个单独使用的锁,是对QMutx的一种简便使用方法。

QMutex更多用于保护一整个对象或一整个代码段,加锁解锁的操作都需要手动进行。而使用QMutexLocker可以很大程度的简化操作,比如在创建时就加锁,对象生命结束时就自动解锁并销毁,常应用于局部使用的情况,所以我称其为局部互斥锁(其实没有这个名称)。

  1. 声明

    QMutexLocker在声明时需要传入一个QMutex指针对象,在声明时自动加锁,在QMutexLocker生命结束时自动解锁并销毁。

    QMutex mtx;
    
    void myFunc(){
       QMutexLocker locker(&mtx); 
       // ...
    }
  2. 常用方法

    • void lock():在 QMutexLocker 对象构造时,自动加锁,所以通常无需显式调用此方法。
    • void unlock():自动在析构时调用,因此无需显式调用。
    • QMutex* mutex():返回所管理的 QMutex 指针。
  3. 代码示例

    #include 
    #include 
    #include 
    
    QMutex mtx;
    
    void threadFunction() {
       QMutexLocker locker(&mtx);  // 自动加锁,作用域结束时自动解锁
       std::cout << "Thread is running with mutex!" << std::endl;
    }
    
    int main() {
       QThread t1, t2;
    
       t1.run(&threadFunction);
       t2.run(&threadFunction);
    
       t1.wait();
       t2.wait();
    
       return 0;
    }
    

4.3 读写锁(QReadWriteLock

QReadWriteLock是一个允许多个线程同时读取共享数据,但在写数据时要求独占访问权限的锁,等同C++中的读写锁。它特别适用于读多写少的场景,能够提高性能,因为多个线程可以并行读取资源。

  1. 声明

    直接调用QReadWriteLock类,同样可以在声明的时候通过递归模式的枚举值QMutex::RecursionMode指定是否使用递归模式,枚举值包含:

    • QMutex::Recursive:递归模式。
    • QMutex::NonRecursive:非递归模式,默认值。

    示例:

    QReadWriteLock rwLocker(QMutex::Recursive);
  2. 常用方法

    • void lockForRead():获取读锁。允许多个线程同时读取数据。
    • bool tryLockForRead(int timeout):尝试获取读锁,参数设置时间限制,单位为毫秒。
    • void lockForWrite():获取写锁。写锁是独占的,只有一个线程可以获得。
    • bool tryLockForWrite(int timeout):尝试获取写锁,参数设置时间限制,单位为毫秒。
    • void unlock():释放锁。
  3. 代码示例

    #include 
    #include 
    #include 
    
    QReadWriteLock lock;
    int sharedData = 0;
    
    void readerFunction() {
       lock.lockForRead();  // 获取读锁
       std::cout << "Read data: " << sharedData << std::endl;
       lock.unlock();  // 释放锁
    }
    
    void writerFunction() {
       lock.lockForWrite();  // 获取写锁
       sharedData++;
       std::cout << "Write data: " << sharedData << std::endl;
       lock.unlock();  // 释放锁
    }
    
    int main() {
       QThread reader1, reader2, writer;
    
       QObject::connect(&reader1, &QThread::started, &readerFunction);
       QObject::connect(&reader2, &QThread::started, &readerFunction);
       QObject::connect(&writer, &QThread::started, &writerFunction);
       // 多个线程同时读,但只能有一个线程进行写
    
       reader1.start();
       reader2.start();
       writer.start();
    
       reader1.wait();
       reader2.wait();
       writer.wait();
    
       return 0;
    }

4.4 原子操作(QAtomic

QAtomic是Qt提供的用于线程同步的原子操作类。它通过硬件支持,提供了无锁的原子操作,如原子加法、减法等,常用于多线程编程中对简单数据类型(如整数)的操作。联想C++中的原子操作,这里只对Qt中的原子操作做一个简单使用介绍。

  1. 声明

    QAtomic通过多个原子操作类来提供对原子类型的操作,最常用的是QAtomicInt类,它对 int 类型的变量提供原子操作。

    #include 
    QAtomicInt counter(0); //初始化为0
  2. 常用方法

    QAtomicInt类为例,也有多个原子操作的方法(方法有很多这里就不一一列举了):

    • bool ref():增加1,等同自增。当然原本的自增运算符++也被重载为原子操作。
    • bool deref():减少1,等同自减。当然原本的自减运算符--也被重载为原子操作。
    • QAtomicInt load():读取值,返回当前值。
    • void store(QAtomicInt newValye):设置为一个新值。
    • QAtomicInt fetchAndAddOrdered(QAtomicInt valueToAdd):指定加数并返回值。
    • ...
  3. 代码示例

    #include 
    #include 
    
    QAtomicInt counter;
    
    void increment() {
       counter.ref(); // 自增
       counter++; // 自增
       counter.fetchAndAddOrdered(5); // 加5
       std::cout << "Atomic counter: " << counter.load() << std::endl;
    }
    
    int main() {
       counter.store(0);
       increment();
       increment();
       return 0;
    }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇