计算机系统应用教程网站

网站首页 > 技术文章 正文

【C++并发编程】(三)互斥锁 互斥锁实现原理

btikc 2024-10-12 13:20:56 技术文章 23 ℃ 0 评论

互斥锁

数据竞争

在并发编程中,数据竞争是指多个线程同时对共享数据进行读写操作,并且至少有一个线程进行写操作,从而导致未定义的行为或结果。 下面给出一个例子:

#include <iostream>  
#include <thread>  

  
int counter = 0; // 共享数据
  
void increment() {  
    for (int i = 0; i < 100000; ++i) {  
        ++counter; // 修改被共享的数据
    }  
}  
  
int main() {  
    std::thread th1(increment);  
    std::thread th2(increment);  
  
    th1.join();  
    th2.join();  
    // 预期输出是200000,但由于数据竞争,实际输出可能小于这个值  
    std::cout << "Final counter value: " << counter << std::endl;  
    // Final counter value: 107194
    
    return 0;  
}

两个线程t1和t2可能同时对counter进行自增操作,导致counter的值增加的数量少于预期(即少于200000)。这是因为:

自增操作包含三个步骤:

  • 从内存中读取counter的当前值0。
  • 将这个值加1。
  • 将结果1写回内存中的counter。

如果两个线程同时执行这些步骤,可能会发生以下情况:

  • 线程t1读取counter的值为0。
  • 线程t2也读取counter的值为0(因为线程t1还没有将新的值写回内存)。
  • 线程t1将1写回counter。
  • 线程t2也将1写回counter(因为它之前读取的值是0)。

C++互斥锁

互斥锁(互斥量)能保护多个线程的共享资源不被同时访问。互斥锁的状态只有两种:开锁(unlocked)和闭锁(locked)。当一个任务(或线程)持有互斥锁,且该互斥锁处于闭锁状态,那么这个任务可以执行访问共享资源的代码,其他任务(或线程)则不被允许。开锁后,该线程将失去了对互斥锁的所有权,其他正在等待的线程现在有机会获取这个互斥锁,以访问共享资源。

在C++11中提供了std::mutex支持这一功能。std::mutex是一个简单的互斥锁类,它有两个主要操作:lock()和unlock(),用于实现闭锁和开锁。

lock():当一个线程调用 lock() 函数时,它会尝试获取与该函数关联的互斥锁。如果互斥量当前没有被其他线程锁定(即它是可用的),则调用线程会成功获取锁,并继续执行其后续代码。 如果互斥量已经被其他线程锁定,则调用线程会被阻塞(即它会停止执行,直到锁变得可用)。 一旦线程获取了锁,它就可以安全地访问共享资源,而不必担心其他线程同时修改它。

unlock(): 当一个线程完成对共享资源的访问后,它应该调用 unlock() 函数来释放与该函数关联的互斥量(或其他锁)。释放锁允许其他线程获取该锁并访问共享资源。

在C++中,直接使用 lock() 和 unlock() 函数来管理锁可能会导致一些问题,特别是当代码路径变得复杂时。为了简化锁的管理并减少出错的可能性,C++11引入了 std::lock_guard 和 std::unique_lock 等RAII(Resource Acquisition Is Initialization)风格的包装器,用于自动管理互斥锁的开锁和闭锁。其中,std::lock_guard 是最简单的锁包装器,它只提供了基本的锁定和解锁功能。 std::unique_lock 提供了比 std::lock_guard 更多的功能,更灵活,后续再单独介绍。

下面给出示例展示如何使用lock()和unlock()以及锁包装器std::lock_guard 进行互斥锁的闭锁和开锁。

lock() 和 unlock()

#include <iostream>  
#include <thread>  
#include <mutex>  
  
std::mutex mtx; // 创建一个互斥锁实例,用于保护共享数据
int counter = 0; // 共享数据 
  
void increment() {  
    for (int i = 0; i < 100000; ++i) {  
     // 显式地调用了lock()和unlock()来分别获取和释放锁:
        mtx.lock(); // 获取锁  
        ++counter; // 递增操作  
        mtx.unlock(); // 释放锁  
    }  
}  
  
int main() {  
    std::thread th1(increment);  
    std::thread th2(increment);  
  
    th1.join();  
    th2.join();  
  
    std::cout << "Final counter value: " << counter << std::endl;  
    // Final counter value: 200000
    
  
    return 0;  
}

std::lock_guard

#include <iostream>  
#include <thread>  
#include <mutex>  
  
std::mutex mtx; // 互斥锁用于保护共享数据
int counter = 0; // 共享数据 
  
void increment() {  
    std::unique_lock<std::mutex> lock(mtx); // 构造时自动获取锁  
    for (int i = 0; i < 100000; ++i) {  
        ++counter; // 递增操作,因为lock存在,所以此操作是安全的  
    }  
    // lock在离开作用域时自动释放锁  
}  
  
int main() {  
    std::thread th1(increment);  
    std::thread th2(increment);  
  
    th1.join();  
    th2.join();  
  
    std::cout << "Final counter value: " << counter << std::endl;  
    // Final counter value: 200000
  
    return 0;  
}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表