C++互斥锁原理以及实际使用介绍

2023-05-17 08:05:00 原理 互斥 介绍

一、互斥原理(mutex)

互斥锁可以确保在任何时候只有一个线程能够进入临界区。当线程需要进入临界区时,它会尝试获取互斥锁的所有权,如果互斥锁已经被其他线程占用,那么当前线程就会进入阻塞状态,直到互斥锁被释放为止。简单说就是一块区域只能被一个线程执行。

当一个线程获取到互斥锁的所有权后,它就可以进入临界区进行操作,当操作完成后,它需要释放互斥锁,让其他线程有机会进入临界区。

下面是一个简单的互斥锁的示例代码,它演示了如何使用 std::mutex 类来保护临界区:

 
#include <iOStream>
#include <thread>
#include <mutex>
 
// 定义互斥锁
std::mutex g_mutex;
 
// 临界区代码
void critical_section(int thread_id) {
    // 加锁
    g_mutex.lock();
    // 访问共享资源
    std::cout << "Thread " << thread_id << " enter critical section." << std::endl;
    // 释放锁
    std::this_thread::sleep_for(std::chrono::seconds(5));
    g_mutex.unlock();
}
 
int main() {
    // 创建两个线程
    std::thread t1(critical_section, 1);
    std::thread t2(critical_section, 2);
    // 等待两个线程执行完成
    t1.join();
    t2.join();
    return 0;
}

main中创建两个线程去访问资源,但是其中一个需要等待另一个线程5s释放后才能访问,形成对资源的锁定。

上面的例子使用的是std::mutex实现互斥锁,需要注意这个互斥锁的声明需要相对的全局变量,也就是说对于使用锁的部分它必须是“全局的”。

二、递归互斥量(Recursive Mutex)

c++ 中的递归互斥量(Recursive Mutex)是一种特殊的互斥量,它可以被同一个线程多次锁定,而不会发生死锁。递归互斥量的实现原理是,在锁定时维护一个锁定计数器,每次解锁时将计数器减一,只有当计数器为 0 时才会释放锁。

以下是递归互斥量的示例代码:

#include <iostream>
#include <thread>
#include <mutex>
 
std::recursive_mutex mtx;
 
void foo(int n) {
    mtx.lock();
    std::cout << "Thread " << n << " locked the mutex." << std::endl;
    if (n > 1) {
        foo(n - 1);
    }
    std::cout << "Thread " << n << " unlocked the mutex." << std::endl;
    mtx.unlock();
}
 
int main() {
    std::thread t1(foo, 3);
    std::thread t2(foo, 2);
    t1.join();
    t2.join();
    return 0;
}

在上面的代码中,我们定义了一个递归函数 foo(),它接受一个整数参数 n,表示当前线程的编号。在函数中,我们首先使用递归互斥量 mtx 锁定当前线程,然后输出一条带有线程编号的信息,接着判断如果 n 大于 1,则递归调用 foo() 函数,并将参数减一。最后,我们输出一条解锁信息,并将递归互斥量解锁。

在主函数中,我们创建了两个线程 t1 和 t2,分别调用 foo() 函数,并传入不同的参数值。由于递归互斥量可以被同一个线程多次锁定,因此在 t1 线程中对 mtx 进行了两次锁定,而在 t2 线程中只进行了一次锁定。

运行结果:

可以看到,递归互斥量可以被同一个线程多次锁定,并且在解锁时必须对应减少锁定计数器。这种机制可以避免死锁的发生,但也需要注意使用时的线程安全问题。

三、读写锁(Read-Write Lock)

读写锁(Read-Write Lock)是一种特殊的互斥锁,用于在多线程环境下对共享资源进行读写操作。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁的使用可以提高并发性能,特别是当读操作比写操作频繁时。

在 C++ 中,读写锁可以通过 std::shared_mutex 类型来实现。下面是一个简单的示例代码,演示了如何使用读写锁来保护一个共享的整型变量:

#include <iostream>
#include <thread>
#include <chrono>
#include <shared_mutex>
 
std::shared_mutex rw_lock; // 读写锁
int shared_var = 0; // 共享变量
 
// 写线程函数
void writer() {
    for (int i = 0; i < 10; ++i) {
        // 独占写锁
        std::unique_lock<std::shared_mutex> lock(rw_lock);
 
        // 写共享变量
        ++shared_var;
        std::cout << "Writer thread: write shared_var=" << shared_var << std::endl;
 
        // 等待一段时间
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
 
// 读线程函数
void reader(int id) {
    for (int i = 0; i < 10; ++i) {
        // 共享读锁
        std::shared_lock<std::shared_mutex> lock(rw_lock);
 
        // 读共享变量
        int value = shared_var;
        std::cout << "Reader thread " << id << ": read shared_var=" << value << std::endl;
 
        // 等待一段时间
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
 
int main() {
    std::thread t1(writer);
    std::thread t2(reader, 1);
    std::thread t3(reader, 2);
    std::thread t4(reader, 3);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    return 0;
}

在上面的代码中,我们定义了一个共享变量 shared_var 和一个读写锁 rw_lock。写线程函数 writer() 独占写锁,对 shared_var 进行自增操作,并输出当前的值。读线程函数 reader() 共享读锁,读取 shared_var 的值,并输出当前的值。所有的线程都会等待一段时间,以模拟实际的操作。

在主函数中,我们创建了一个写线程和三个读线程。由于读写锁的特性,读线程可以并发读取共享变量,而写线程会独占写锁,只有在写操作完成之后,读线程才能再次读取共享变量。因此,输出结果中读线程的顺序可能会有所不同,但是写线程的操作一定是顺序执行的。

注意,这里使用 std::unique_lockstd::shared_mutex 类型的对象来获取独占写锁,使用 std::shared_lockstd::shared_mutex 类型的对象来获取共享读锁。这些锁对象会在作用域结束时自动解锁,避免了手动解锁的问题。

四、条件变量(Condition Variable)

条件变量(Condition Variable)是一种线程间同步机制,用于在某些特定条件下阻塞或唤醒线程。在 C++ 中,条件变量是通过 std::condition_variable 类来实现的。

下面是一个使用条件变量的示例代码,其中有两个线程,一个线程不停地生产数据,另一个线程则等待数据,当有数据可用时,将数据进行消费。

#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>
 
std::queue<int> data_queue; // 数据队列
std::mutex data_mutex; // 互斥锁
std::condition_variable data_cond; // 条件变量
 
// 生产数据函数
void producer() {
    for (int i = 1; i <= 10; ++i) {
        // 生产数据
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::unique_lock<std::mutex> lock(data_mutex);
        data_queue.push(i);
        std::cout << "Producer thread: produce data " << i << std::endl;
 
        // 唤醒消费线程
        data_cond.notify_one();
    }
}
 
// 消费数据函数
void consumer() {
    while (true) {
        // 等待数据
        std::unique_lock<std::mutex> lock(data_mutex);
        data_cond.wait(lock, [] { return !data_queue.empty(); });
 
        // 消费数据
        int data = data_queue.front();
        data_queue.pop();
        std::cout << "Consumer thread: consume data " << data << std::endl;
 
        // 检查是否结束
        if (data == 10) {
            break;
        }
    }
}
 
int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

在上面的代码中,我们定义了一个数据队列 data_queue 和一个互斥锁 data_mutex,同时定义了一个条件变量 data_cond。生产数据的函数 producer() 不停地往队列中添加数据,每次添加完数据之后,通过调用 data_cond.notify_one() 唤醒等待的消费线程。消费数据的函数 consumer() 通过调用 data_cond.wait(lock, [] { return !data_queue.empty(); }) 来等待数据,当队列中有数据时,将数据从队列中取出并消费,如果取出的数据是最后一个,则退出循环。

在主函数中,我们创建了一个生产线程和一个消费线程。生产线程生产 10 个数据,消费线程从队列中消费数据,直到消费到最后一个数据为止。

注意,这里使用了 std::unique_lockstd::mutex 类型的对象来获取互斥锁,并使用 lambda 表达式 [] { return !data_queue.empty(); } 来判断条件是否满足。在调用 wait() 函数时,当前线程会阻塞,直到条件变量被其他线程唤醒或超时。当 wait() 函数返回时,当前线程会重新获取互斥。

简单一些的例子:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <condition_variable>
 
bool ready = false; // 条件变量
std::mutex data_mutex; // 互斥锁
std::condition_variable data_cond; // 条件变量
 
void do_something() {
    // 模拟工作
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
 
void waiting_thread() {
    // 等待条件变量
    std::unique_lock<std::mutex> lock(data_mutex);
    data_cond.wait(lock, [] { return ready; });
 
    // 条件满足后输出一句话
    std::cout << "Condition satisfied, waiting thread resumes." << std::endl;
    do_something();
}
 
int main() {
    std::thread t1(waiting_thread);
 
    // 模拟条件满足后的操作
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    {
        std::lock_guard<std::mutex> lock(data_mutex);
        ready = true;
        data_cond.notify_one();
    }
 
    t1.join();
    return 0;
}

在上面的代码中,我们定义了一个条件变量 ready 和一个互斥锁 data_mutex,同时定义了一个条件变量 data_cond。等待条件变量的函数 waiting_thread() 首先获取互斥锁,然后通过调用 data_cond.wait(lock, [] { return ready; }) 等待条件变量,当 ready 为 true 时,线程会被唤醒,输出一句话,并模拟一些工作的操作。在主函数中,我们创建了一个等待条件变量的线程 t1,然后模拟条件满足后的操作,即将 ready 设置为 true,然后通过调用 data_cond.notify_one() 唤醒等待的线程。 

五、总结

互斥锁保证了计算机资源访问的安全,互斥锁的不当使用同时也加大了程序阻塞的风险。

提前祝大家五一前工作生活学习一切顺利。

到此这篇关于C++ 互斥锁原理以及实际使用介绍的文章就介绍到这了,更多相关C++ 互斥锁原理及使用内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章