c++进阶篇——初窥多线程(四) 线程同步的概念以及锁

什么是线程同步

线程同步是指多个线程在执行过程中,由于共享资源,导致数据不一致的问题。线程同步是为了解决多个线程同时访问共享资源时,由于线程切换导致的数据不一致问题,大家可能不是很理解为什么要线程同步,我们举个简单的例子:

#include <iostream>
#include <thread>using namespace std;int counter=0;void* ADD_COUNT()
{for(int i=0;i<100000;i++){counter++;}
}int main()
{thread a(ADD_COUNT);thread b(ADD_COUNT);a.join();b.join();cout<<"counter="<<counter<<endl;
}

上面代码我这里的运行结果是161335,而不是200000,这就是线程同步的问题,这里我们的counter是共享资源,而a,b两个线程同时访问counter,导致数据不一致,讲的通俗一点,在某个时刻,a和b线程都读到了Count=n,这是它们都会执行count=n+1,然后a线程执行完了,b线程执行完了,最终的结果是n+1,而不是n+2,导致最后的结果小于200000,而为了解决这个问题,我们就需要线程同步,让a,b两个线程不能同时访问共享资源,这样就可以才能保证数据的一致性。

如何实现线程同步

在讲解如何进行线程同步之前,我们要进行线程同步主要是为了防止多个线程同时访问共享资源而造成混乱,所以无论我们采用什么样的手段目的只有一个:保证共享资源在同一时刻只有一个线程访问.

那什么是共享资源呢?所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。我们在进行线程同步时所需要做的就是找到这些临界资源,然后保证同一时刻只有一个线程访问这些临界资源.

常见实现线程同步的方法主要有下面几种:

  • 条件变量
  • 信号量

而今天我们介绍的就是——锁.

锁的概念

锁,在我们日常生活中是用来保护我们的私有财产保证其不被他人侵犯,而在计算机中,锁也用来保护资源的,它用来保护共享资源不被多个线程同时访问,保证共享资源在同一时刻只有一个线程访问,从而保证数据的一致性。

互斥锁

互斥锁的原理

互斥锁是一种用于多线程编程中,防止多个线程同时访问共享资源的同步机制。互斥锁可以确保在任何时刻只有一个线程可以访问共享资源,从而避免了数据竞争和一致性问题。它的原理很简单:首先一个共享资源开始是没有锁的,当一个线程想要访问这个共享资源的时候,它会先尝试看这个资源有没有锁,如果没有就可以正常对这个共享资源进行操作,如果有锁,那么这个线程就只能等待,直到锁被释放为止。

互斥锁的种类

cpp11给我们提供了四种互斥锁的类型:

std::mutex mtx; //最简单的互斥锁,不能递归使用
std::recursive_mutex r_mtx; //递归互斥锁
std::timed_mutex t_mtx; //定时互斥锁,可以设置等待时间
std::recursive_timed_mutex rt_mtx; //定时递归互斥锁

我们接下来依次来看看这四种锁的使用方法:

  • mutex
#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>using namespace std;int counter=0;
mutex mtx; //互斥锁void* ADD_COUNT()
{for(int i=0;i<100000;i++){mtx.lock(); //加锁,若加锁失败线程会阻塞  除此之外我们还可以加try_lock()尝试加锁,如果加锁失败,则不会阻塞counter++;mtx.unlock(); //解锁,若解锁失败,则线程会阻塞//     this_thread::sleep_for(chrono::milliseconds(1)); //休眠1毫秒}
}int main()
{thread a(ADD_COUNT);thread b(ADD_COUNT);a.join();b.join();cout<<"counter="<<counter<<endl;
}

上面就是一个简单的mutex的例子,我们创建了两个线程,然后让这两个线程去增加一个共享资源,但是这个共享资源被一个互斥锁保护着,所以这两个线程不能同时访问这个共享资源,只能一个一个的来,这样就保证了数据的一致性。而除此之外还有一些其他的函数api:

  1. try_lock尝试加锁,如果加锁失败,则不会阻塞
  2. lock加锁,如果加锁失败,则线程会阻塞
  3. unlock解锁,如果解锁失败,则线程会阻塞
  • timed_mutex
    相对于mutex,timed_mutex多了一个等待时间,如果等待时间到了,还没有加锁成功,那么就会返回false,否则就会返回true,并且会自动解锁
#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>using namespace std;chrono::seconds timeout(1); // 超时时间
timed_mutex mtx;void work()
{while(true){if(mtx.try_lock_for(timeout)){cout<<"线程"<<this_thread::get_id()<<"获取锁"<<endl;this_thread::sleep_for(chrono::seconds(10));mtx.unlock();break;}else{cout<<"线程"<<this_thread::get_id()<<"正在尝试获取锁......."<<endl;this_thread::sleep_for(chrono::seconds(1));}}
}int main()
{thread t1(work);thread t2(work);t1.join();t2.join();return 0;
}

还有一些其他的api:

  1. try_lock_until尝试加锁,如果加锁失败,则不会阻塞,直到指定的时间点
  • recursive_mutex
    递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题,在下面的例子中使用独占非递归互斥量会发生死锁:
#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>using namespace std;class Calculate
{int m_i;mutex mtx;public:Calculate():m_i(6){}void mul(){mtx.lock();m_i *= 2;}void div(){mtx.lock();m_i /= 2;}void test(){mul();div();}
};int main()
{Calculate c;c.test();return 0;
}

当我们尝试获取多个锁的时候会出现死锁的情况(什么是死锁我们后面会介绍),所以使用递归锁可以解决这个问题:

#include <iostream>
#include <mutex>
#include <chrono>
#include <thread>using namespace std;class Calculate
{int m_i;recursive_mutex mtx;public:Calculate():m_i(6){}void mul(){mtx.lock();m_i *= 2;}void div(){mtx.lock();m_i /= 2;}void test(){mul();div();show();}void show(){cout << m_i << endl;}
};int main()
{Calculate c;c.test();return 0;
}
  • resursive_timed_mutex
    这个锁和recursive_mutex类似,但是它提供了超时机制,如果获取锁超时了,就会返回false,否则返回true,这个就不做过多介绍大家可以参考mutextimed_mutex的用法。

读写锁

读写锁的介绍

读写锁是一种特殊的互斥锁,我们可以将读写锁看做读锁与写锁,写锁允许多个线程同时读取共享资源,但是只允许一个线程写入共享资源。写锁在写入共享资源时,会阻塞其他线程的读取操作。这样可以保证数据的一致性,
同时避免了多个线程同时同时对共享资源进行不同操作导致的冲突,同时多个线程可以同时读取共享资源,提高了并发性能。

读写锁的实现原理是使用一个计数器来记录当前有多少个线程正在读取/写入共享资源,如果计数器为0,那么就可以写入/读取共享资源,否则就不能写入/读取共享资源。

读写锁的用法

读写锁的常用函数主要有以下几个:

- pthread_rwlock_t rwlock_init(void); // 初始化读写锁
- int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); // 销毁读写锁
- int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 获取读锁
- int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 获取写锁
- int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 释放锁

下面是一个简单的读写锁的例子:

#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <random>
#include <pthread.h>pthread_rwlock_t rwlock;
int count = 0;
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, 4999);void read(int id) {pthread_rwlock_rdlock(&rwlock); // 获取读锁std::cout << "线程 " << id << " 拿到读锁" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(distribution(generator)));std::cout << "线程 " << id << " 释放读锁" << std::endl;pthread_rwlock_unlock(&rwlock); // 释放读锁
}void write(int id) {pthread_rwlock_wrlock(&rwlock); // 获取写锁std::cout << "线程 " << id << " 拿到写锁" << std::endl;int temp = count;std::this_thread::sleep_for(std::chrono::milliseconds(distribution(generator) % 1000));count = temp + 1;std::cout << "线程 " << id << " 释放写锁 " << count << std::endl;pthread_rwlock_unlock(&rwlock); // 释放写锁
}void runThreads(int readCount, int writeCount) {std::vector<std::thread> threads;for (int i = 0; i < readCount; ++i) {threads.emplace_back(read, i);}for (int i = 0; i < writeCount; ++i) {threads.emplace_back(write, i + readCount);}for (auto& th : threads) {th.join();}
}int main() {pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);const int readCount = 7;const int writeCount = 3;runThreads(readCount, writeCount);std::cout << "最终结果: " << count << std::endl;pthread_rwlock_destroy(&rwlock);return 0;
}

lock_guard以及unique_lock

lock_guard

互斥锁的获取和释放必须成对出现,而一旦出现线程在获取互斥锁而没有释放时,其它线程将永远等待!(使用 lock() 的情况下)为了解决此类情况,C++ 标准库提供了一个 RAII 式(什么是RAII我们后面会有所介绍)的方案,即模板类 lock_guard。
该类需要传入一个泛型参数,表示锁的类型,同时该类需要传入一个具体的锁,这样你仅需要初始化一个 lock_guard, 线程将自动获取锁,同时在线程结束时,临时变量被销毁,同时锁也会被释放。

#include <iostream>
#include <mutex>
#include <thread>using namespace std;int a = 1;
mutex a_mutex;void increase() {lock_guard<mutex> lg(a_mutex);this_thread::sleep_for(2000ms);++a;
}int main() {thread t1(increase);thread t2(increase);t1.join();t2.join();cout << a << endl; // 3return 0;
}
unique_lock

上面我们已经介绍了lock_guard,lock_guard 虽然方便,却仍有其不便之处,那就是锁的粒度太大了,在 lock_guard 中,上锁发现在初始化过程,而释放锁则需要在对象被销毁时,这样显然是不够灵活的。所以这里我们要介绍一个更灵活的模板类 unique_lock,它允许我们自定义锁的粒度,同时它还支持 RAII 的特性,即锁的获取和释放。

它具有下面这些特性:

  1. 延迟锁定(deferred locking)
  2. 尝试锁定 (attempts at locking)
  3. 超时机制 (time-constrained)
  4. 递归锁定 (recursive locking)
  • 转移互斥锁的所有权 (transfer of lock ownership)
  • 与条件变量一起使用 (use with condition variables)

不过unique_lock只是一个灵活的锁管理器,它并不能直接对互斥锁进行上锁和解锁操作,它需要与互斥锁配合使用,对锁进行包装,接下来我们来看一下他是怎么实现它的这些特性的:

  • 延迟锁定
void increase() {unique_lock<mutex> ul(a_mutex, defer_lock); // 延迟锁定,初始化时并不上锁,需要我们手动上锁this_thread::sleep_for(2000ms);ul.lock(); // 手动上锁++a;
}
  • 尝试锁定
void increase() {unique_lock<mutex> ul(a_mutex, try_to_lock); // 尝试锁定,如果锁已经被其他线程占用,则返回false,否则返回trueif(!ul.owns_lock()) {this_thread::sleep_for(2000ms);cout<<"failed to get mutex"}++a;
}
  • 超时机制
void increase() {unique_lock<timed_mutex> ul(a_mutex, defer_lock); if(!ul.try_lock_for(2s)) {cout<<"timeout";<<endl;}++a;
}
  • 递归锁定
  void once() {unique_lock<recursive_mutex> ul(m);++shared;cout << "once\n";}void twice() {unique_lock<recursive_mutex> ul(m);for (int i = 0; i < 2; i++) {cout << "twice: ";once();}}
  • 移交所有权
void func() {unique_lock ul(m);unique_lock ul2 = move(ul); // 移动构造,此时锁的所有权已经转移给ul2ul2.swap(ul);               // 交换所有权,结束后ul有锁,ul2不再拥有锁ul.release();               // 释放所有权,此时ul不再拥有锁++a;m.unlock(); // unlock the m mutex manually
}

shared_mutex

shared_mutex 是 C++17 引入的新的互斥锁类型,它允许多个线程同时拥有读权限,但只有一个线程可以拥有写权限。shared_mutex 提供了以下功能:

  • lock():获取独占锁,阻塞直到成功获取锁。
  • try_lock():尝试获取独占锁,如果失败则立即返回 false。
  • unlock():释放独占锁。
  • lock_shared():获取共享锁,阻塞直到成功获取锁。
  • try_lock_shared():尝试获取共享锁,如果失败则立即返回 false。
  • unlock_shared():释放共享锁。

这里我们就不得不说一下什么是独占锁和共享锁了

和我们日常生活的独占与共享一样,独占锁就是只能一个线程访问,而共享锁就是多个线程可以访问,当我们读取的时候,可以多个线程同时读取,但是写入的时候,只能有一个线程写入,其他线程需要等待写入完成才能继续写入。在读多写少的场景下,shared_mutex 可以提高并发性能。

示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>using namespace std;int a = 1;
shared_mutex sm;int read() {shared_lock<shared_mutex> sl(sm);cout << "read\n";this_thread::sleep_for(2s);return a;
}int main() {thread t1(read);thread t2(read);thread t3(read);thread t4(read);t1.join();t2.join();t3.join();t4.join();return 0;
}

大家可以尝试运行一下代码,会发现2szuong 4 个线程同时读取了 a 的值,而不是是按照顺序读取的。

死锁问题

死锁的产生原因

死锁的原因在于两个线程同时获取多个锁且存在获取的锁被对方占用的时候,这个时候线程将永远处于阻塞状态,比如下面这样:

#include <iostream>
#include <mutex>
#include <thread>using namespace std;int a = 1, b = 1;
mutex a_mutex, b_mutex;void increase1() {lock_guard<mutex> lg1(a_mutex);this_thread::sleep_for(1000ms);++a;lock_guard<mutex> lg2(b_mutex);++b;
}
void increase2() {lock_guard<mutex> lg1(b_mutex);this_thread::sleep_for(1000ms);++b;lock_guard<mutex> lg2(a_mutex);++a;
}int main() {thread t1(increase1);thread t2(increase2);t1.join();t2.join();cout << a << endl << b;return 0;
}

在上面的代码中t1在获取完a锁,t2在获取完b锁之后,t1需要获取b锁,t2需要获取a锁,但是a锁和b锁都被对方占用,导致两个线程都处于阻塞状态,程序将永远无法结束,而这也就是我们所说的死锁了。

预防死锁的方法

  • 避免多次锁定, 多检查

  • 对共享资源访问完毕之后, 一定要解锁,或者在加锁的使用 trylock

  • 如果程序中有多把锁, 可以控制对锁的访问顺序(顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。

  • 项目程序中可以引入一些专门用于死锁检测的模块

参考文章与链接

线程同步

cppreference

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/438344.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Stable Diffusion WebUI中安装SadTalker插件时几种错误提示的处理方法

SD中的插件一般安装比较简单&#xff0c;但也有一些插件安装会比较难。比如我在安装SadTalker时&#xff0c;就遇到很多问题&#xff0c;一度放弃了&#xff0c;后来查了一些网上攻略&#xff0c;自己也反复查看日志&#xff0c;终于解决&#xff0c;不吐不快。 一、在Stable …

15分钟学 Python :编程工具 Idea 和 vscode 中配置 Python ( 补充 )

编程工具配置 Python 在 IDE 和 VSCode 中 在编程学习的过程中&#xff0c;选择合适的开发工具至关重要。本文将详细介绍在两种流行的IDE&#xff08;IntelliJ IDEA 和 Visual Studio Code&#xff09;中如何配置Python环境&#xff0c;帮助你更高效地进行Python开发。 一、编…

基于SSM的出租车租赁管理系统的设计与实现

文未可获取一份本项目的java源码和数据库参考。 1 选题的背景 现代社会&#xff0c;许多个人、家庭&#xff0c;因为生活、工作方式的改变&#xff0c;对汽车不再希望长期拥有&#xff0c;取而代之的是希望汽车能“召之即…

开源且实用的C#/.NET编程技巧练习宝库(学习,工作,实践指南)

DotNet Exercises介绍 DotNetGuide专栏C#/.NET/.NET Core编程常用语法、算法、技巧、中间件、类库、工作业务实操练习集&#xff0c;配套详细的文章教程讲解&#xff0c;助你快速掌握C#/.NET/.NET Core中各种编程常用语法、算法、技巧、中间件、类库、工作业务实操等等。 GitH…

【Spring Boot 入门二】Spring Boot中的配置文件 - 掌控你的应用设置

一、引言 在上一篇文章中&#xff0c;我们开启了Spring Boot的入门之旅&#xff0c;成功构建了第一个Spring Boot应用。我们从环境搭建开始&#xff0c;详细介绍了JDK的安装以及IDE的选择与配置&#xff0c;然后利用Spring Initializr创建了项目&#xff0c;分析了项目结构&am…

黑马linux笔记(转载)

学习链接 视频链接&#xff1a;黑马程序员新版Linux零基础快速入门到精通 原文链接&#xff1a;黑马程序员新版Linux零基础快速入门到精通——学习笔记 黑马Linux笔记 文章目录 学习链接01初识Linux1.1、操作系统概述1.1.1、硬件和软件1.1.2、操作系统1.1.3、常见操作系统 1.…

SSM人才信息招聘系统-计算机毕业设计源码28084

摘要 本研究旨在基于Java和SSM框架设计并实现一个人才信息招聘系统&#xff0c;旨在提升招聘流程的效率和精准度。通过深入研究Java和SSM框架在Web应用开发中的应用&#xff0c;结合人才招聘领域的需求&#xff0c;构建了一个功能完善、稳定高效的招聘系统。利用SSM框架的优势&…

数据订阅与消费中间件Canal 服务搭建(docker)

MySQL Bin-log开启 进入mysql容器 docker exec -it mysql5.7 bash开启mysql的binlog cd /etc/mysql/mysql.conf.dvi mysqld.cnf #在文件末尾处添加如下配置&#xff08;如果没有这个文件就创建一个&#xff09; [mysqld] # 开启 binlog log-binmysql-bin #log-bin/var/lib/mys…

CSP-J模拟赛三补题报告

前言 挂了110pts( ⇑ \Uparrow ⇑ \hspace{14em} 有史以来最大傻逼 T1&#xff1a; 100 p t s \color{green}100pts 100pts T2: 100 p t s → 80 p t s \color{green}100pts\color{yellow}\rightarrow\color{red}80pts 100pts→80pts T3: 100 p t s → 10 p t s \color{gre…

k8s架构,从clusterIP到光电半导体,再从clusterIP到企业管理

clusterIP作为k8s中的服务&#xff0c; 也是其他三个服务的基础 ~]$ kubectl create service clusterip externalname loadbalancer nodeport 客户端的流量到service service分发给pod&#xff0c;pod由控制器自动部署&#xff0c;自动维护 那么问题是service的可用…

【C++前缀和】1895. 最大的幻方|1781

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode1895. 最大的幻方 难度分&#xff1a;1781 一个 k x k 的 幻方 指的是一个 k x k 填满整数的方格阵&#xff0c;且每一行、每一列以及两条对角线的和 全部相…

ubuntu 设置静态IP

一、 ip addresssudo nano /etc/netplan/50-cloud-init.yaml 修改前&#xff1a; 修改后&#xff1a; # This file is generated from information provided by the datasource. Changes # to it will not persist across an instance reboot. To disable cloud-inits # ne…

360浏览器时不时打不开csdn

从百度或者csdn的搜索中打开&#xff0c;会发现打不开网页&#xff0c;以前也出现过&#xff0c;只是以为这篇文章被删了&#xff0c;昨天接连多个文章打不开&#xff0c;怀疑的浏览器的问题&#xff0c;复制网址到edge浏览器就打开了 刚刚又出现了&#xff0c;怀疑360会拦截某…

Elasticsearch——数据聚合、数据同步与集群搭建

目录 1.数据聚合1.1.聚合的种类1.2.DSL实现聚合1.2.1.Bucket 聚合语法1.2.2.聚合结果排序1.2.3.限定聚合范围1.2.4.Metric 聚合语法1.2.5.小结 1.3.RestAPI 实现聚合1.3.1.API 语法1.3.2.业务需求1.3.3.业务实现 2.自动补全2.1.拼音分词器2.2.自定义分词器2.3.自动补全查询2.4.…

使用百度文心智能体创建多风格表情包设计助手

文章目录 一、智能定制&#xff0c;个性飞扬二、多元风格&#xff0c;创意无限 百度文心智能体平台为你开启。百度文心智能体平台&#xff0c;创建属于自己的智能体应用。百度文心智能体平台是百度旗下的智能AI平台&#xff0c;集成了先进的自然语言处理技术和人工智能技术&…

C++ STL 初探:打开标准模板库的大门

文章目录 C STL 初探&#xff1a;打开标准模板库的大门前言第一章: 什么是STL&#xff1f;1.1 标准模板库简介1.2 STL的历史背景1.3 STL的组成 第二章: STL的版本与演进2.1 不同的STL版本2.2 STL的影响与重要性 第三章: 为什么学习 STL&#xff1f;3.1 从手动编写到标准化解决方…

C++网络编程之TCP协议

概述 TCP&#xff0c;即传输控制协议&#xff0c;英文全称为Transmission Control Protocol&#xff0c;是互联网协议套件中的核心协议之一。它工作在OSI七层模型的传输层&#xff0c;也工作在TCP/IP四层模型的传输层。TCP协议的主要目的是&#xff1a;在不可靠的网络环境中提供…

腾讯一面-LRU缓存

为了设计一个满足LRU&#xff08;最近最少使用&#xff09;缓存约束的数据结构&#xff0c;我们可以使用哈希表&#xff08;HashMap&#xff09;来存储键值对&#xff0c;以便在O(1)时间复杂度内访问任意键。同时&#xff0c;我们还需要一个双向链表&#xff08;Doubly Linked …

飞创龙门双驱XYZ直线模组高精度应用实例

飞创龙门双驱XYZ直线模组集超精密定位、高动态响应和灵活配置于一体&#xff0c;适用于电子制造行业&#xff08;点胶、组装、检测&#xff09;、半导体圆晶加工、芯片封装、激光切割、激光焊接、数控机床、精密检测及科研实验等&#xff0c;满足高精度、高动态的三维定位需求&…