欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 社会 > C++ 线程安全之互斥锁

C++ 线程安全之互斥锁

2025/1/25 3:38:18 来源:https://blog.csdn.net/qq_74224788/article/details/145281164  浏览:    关键词:C++ 线程安全之互斥锁

目录

线程安全

1. 原子性

2. 可见性

3. 顺序性

  互斥锁

1.mutex类

2.timed_mutex类

3.recursive_mutex类

4.lock_guard类


线程安全

  线程安全是多线程编程是的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。上述是百度百科给出的一个概念解释。换言之,线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了。

要考虑线程安全问题,就需要先考虑并发的三大基本特性原子性、可见性以及顺序性。

1. 原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。

那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

2. 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到,这就是可见性问题。

3. 顺序性

程序执行的顺序按照代码的先后顺序执行,CPU为了整体的执行效率,可能会优化代码的执行顺序,但会确保最终执行结果与不优化的执行结果一致.

例如:  int i=10; i=20;i+=20;i=60;  会自动优化为int i = 60;

可通过在变量前加上volatile关键字使变量具有可见性,并禁止CPU进行代码优化(重排序).

代码示例(非线程安全情况):

#include <iostream>#include <thread>        // 线程类头文件。using namespace std;int aa = 0;     // 定义全局变量。// 普通函数,把全局变量aa加1000000次。void func() {for (int ii = 1; ii <= 1000000; ii++)aa++;}int main(){// 用普通函数创建线程。thread t1(func);     // 创建线程t1,把全局变量aa加1000000次。thread t2(func);     // 创建线程t2,把全局变量aa加1000000次。t1.join();         // 回收线程t1的资源。t2.join();         // 回收线程t2的资源。cout << "aa=" << aa << endl;   // 显示全局变量aa的值。}

  互斥锁

C++11提供了四种互斥锁:

  1. mutex:互斥锁。
  2. timed_mutex:带超时机制的互斥锁。
  3. recursive_mutex:递归互斥锁。
  4. recursive_timed_mutex:带超时机制的递归互斥锁。

包含头文件:#include <mutex>

1.mutex

1)加锁lock()

互斥锁有锁定和未锁定两种状态。

如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。

如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。

2)解锁unlock()

只有持有锁的线程才能解锁。

3)尝试加锁try_lock()

如果互斥锁是未锁定状态,则加锁成功,函数返回true。

如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)

代码示例(cout也是全局对象,为屏幕输出流对象):

#include <iostream>#include <thread>                // 线程类头文件。#include <mutex>                // 互斥锁类的头文件。using namespace std;mutex mtx;        // 创建互斥锁,保护共享资源cout对象。// 普通函数。void func(int bh, const string& str) {for (int ii = 1; ii <= 10; ii++){mtx.lock();      // 申请加锁。cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;mtx.unlock();  // 解锁。this_thread::sleep_for(chrono::seconds(1));     // 休眠1秒。}}int main(){// 用普通函数创建线程。thread t1(func, 1, "我是星穹铁道高手。");thread t2(func, 2, "我是星穹铁道高手。");thread t3(func, 3, "我是星穹铁道高手。");thread t4(func, 4, "我是星穹铁道高手。");thread t5(func, 5, "我是星穹铁道高手。");t1.join();         // 回收线程t1的资源。t2.join();         // 回收线程t2的资源。t3.join();         // 回收线程t3的资源。t4.join();         // 回收线程t4的资源。t5.join();         // 回收线程t5的资源。}

注:无论什么锁,加锁段内的代码在确保线程安全的情况下应当尽量少,越少效率越高,且确保解锁后至下一次加锁前或线程结束前的程序是线程安全的.

2.timed_mutex

增加了两个成员函数(与try_lock类似):

bool try_lock_for(时间长度);bool try_lock_until(时间点);

时间类概念详见之前文章<<C++时间操作chrono库>> 

3.recursive_mutex

递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。

代码示例:

#include <iostream>#include <mutex>        // 互斥锁类的头文件。using namespace std;class AA{recursive_mutex m_mutex;public:void func1() {m_mutex.lock();cout << "调用了func1()\n";m_mutex.unlock();}void func2() {m_mutex.lock();cout << "调用了func2()\n";func1();m_mutex.unlock();}};int main(){AA aa;//aa.func1();aa.func2();}

4.lock_guard

lock_guard是模板类,可以简化互斥锁的使用,也更安全。

lock_guard的定义如下:

template<class Mutex>class lock_guard{explicit lock_guard(Mutex& mtx);}

lock_guard在构造函数中加锁,在析构函数中解锁。

还有个unique_lock<mutex> 用于把互斥锁转换为unique_lock类,本质与之前的mutex互斥锁毫无区别,但是增加了 unlock()成员函数,用于主动解锁,不用像lock_guard类需等待超出作用域后自动调用析构函数解锁(unique_lock类超出作用域后也会自动调用析构函数解锁,手动加{}可以定义作用域范围)

lock_guard用了RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放).智能指针也是采用的RAll思想.   

注:此课件及源代码来自B站up主:码农论坛,该文章仅作为本人学习笔记及交流学习使用,本人仅做整理并补充学习理解和知识点。 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com