常见的“锁”有哪些?
悲观锁
悲观锁认为在并发环境中,数据随时可能被其他线程修改,因此在访问数据之前会先加锁,以防止其他线程对数据进行修改。常见的悲观锁实现有:
1.互斥锁
原理:互斥锁是一种最基本的锁类型,同一时间只允许一个线程访问共享资源。当一个线程获取到互斥锁后,其他线程如果想要访问该资源,就必须等待锁被释放。
应用场景:适用于写操作频繁的场景,如数据库中的数据更新操作。在 C++ 中可以使用 std::mutex
来实现互斥锁,示例代码如下:
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;
int sharedResource = 0;void increment() {std::lock_guard<std::mutex> lock(mtx);sharedResource++;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Shared resource: " << sharedResource << std::endl;return 0;
}
2.读写锁
原理:读写锁允许多个线程同时进行读操作,但在进行写操作时,会独占资源,不允许其他线程进行读或写操作。读写锁分为读锁和写锁,多个线程可以同时获取读锁,但写锁是排他的。
应用场景:适用于读多写少的场景,如缓存系统。在 C++ 中可以使用 std::shared_mutex 来实现读写锁,示例代码如下:
#include <iostream>
#include <shared_mutex>
#include <thread>std::shared_mutex rwMutex;
int sharedData = 0;void readData() {std::shared_lock<std::shared_mutex> lock(rwMutex);std::cout << "Read data: " << sharedData << std::endl;
}void writeData() {std::unique_lock<std::shared_mutex> lock(rwMutex);sharedData++;std::cout << "Write data: " << sharedData << std::endl;
}int main() {std::thread t1(readData);std::thread t2(writeData);t1.join();t2.join();return 0;
}
乐观锁
乐观锁是一种在多线程环境中避免阻塞的同步技术,它假设大部分操作是不会发生冲突的,因此在操作数据时不会直接加锁,而是通过检查数据是否发生了变化来决定是否提交。如果在提交数据时发现数据已被其他线程修改,则会放弃当前操作,重新读取数据并重试。
应用场景:适用于读多写少、冲突较少的场景,如电商系统中的库存管理。
在 C++ 中,乐观锁的实现通常依赖于版本号或时间戳的机制。每个线程在操作数据时,会记录数据的版本或时间戳,操作完成后再通过比较版本号或时间戳来判断是否发生了冲突。
下面是一个使用版本号实现乐观锁的简单示例代码:
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>// 共享数据结构
struct SharedData {int value; // 数据的实际值std::atomic<int> version; // 数据的版本号,用于检查是否发生了修改
};// 线程安全的乐观锁实现
bool optimisticLockUpdate(SharedData& data, int expectedVersion, int newValue) {// 检查数据的版本号是否与预期一致if (data.version.load() == expectedVersion) {// 进行数据更新data.value = newValue;// 增加版本号data.version.fetch_add(1, std::memory_order_relaxed);return true; // 成功提交更新}return false; // 数据版本不一致,操作失败
}void threadFunction(SharedData& data, int threadId) {int expectedVersion = data.version.load();int newValue = threadId * 10;std::cout << "Thread " << threadId << " starting with version " << expectedVersion << "...\n";std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作// 尝试更新数据if (optimisticLockUpdate(data, expectedVersion, newValue)) {std::cout << "Thread " << threadId << " successfully updated value to " << newValue << "\n";} else {std::cout << "Thread " << threadId << " failed to update (version mismatch)\n";}
}int main() {// 初始化共享数据,值为 0,版本号为 0SharedData data{0, 0};// 启动多个线程进行乐观锁测试std::thread t1(threadFunction, std::ref(data), 1);//std::ref(data) 将 data 包装成一个引用包装器,确保 data 在传递给函数时以引用的方式传递,而不是被复制。std::thread t2(threadFunction, std::ref(data), 2);std::thread t3(threadFunction, std::ref(data), 3);t1.join();t2.join();t3.join();std::cout << "Final value: " << data.value << ", Final version: " << data.version.load() << "\n";return 0;
}
原子锁
原子锁是一种基于原子操作(如CAS、test_and_set)的锁机制。与传统的基于互斥量(如 std::mutex
)的锁不同,原子锁依赖于硬件提供的原子操作,允许对共享资源的访问进行同步,且通常比传统锁更加高效。它通过原子操作保证对共享资源的独占访问,而不需要显式的线程调度。
原子锁的适用场景:
1.简单数据类型:原子锁最常用于锁定简单的基础数据类型,例如整数、布尔值、指针等。通过原子操作,多个线程可以安全地对这些数据进行读写,而不会发生数据竞争。
示例:std::atomic<int>, std::atomic<bool>, std::atomic<long long>
2.计数器、标志位:当需要在多线程中维护计数器、标志位或状态变量时,原子操作非常合适。例如,当多个线程需要递增计数器时,可以用原子操作避免使用传统的互斥锁。
示例:使用 std::atomic<int> 来维护线程安全的计数器。
注:原子锁通常不能锁容器类型。
什么是原子操作?
原子操作是指不可分割的操作,在执行过程中不会被中断或干扰。原子操作保证了操作的完整性,要么完全执行,要么完全不执行,避免了在操作过程中被线程切换打断,从而避免了数据竞争和不一致的情况。
1.自旋锁
什么是自旋锁?
自旋锁是一种使用原子操作来检测锁是否可用的锁机制。自旋锁是一种忙等待的锁,当线程尝试获取锁失败时,会不断地检查锁的状态,直到成功获取锁。
在 C++ 中,可以使用 std::atomic_flag
结合 test_and_set
操作来实现一个简单的自旋锁:
test_and_set
是一个原子操作,它会检查一个布尔标志的值,然后将该标志设置为 true
。整个操作过程是不可分割的,即不会被其他线程的操作打断。这个布尔标志通常被用作锁,线程通过检查并设置这个标志来尝试获取锁。
工作原理
- 检查标志状态:线程首先检查布尔标志的当前值。
- 设置标志为 true:如果标志当前为 false,表示锁未被占用,线程将标志设置为 true,表示成功获取到锁;如果标志当前为 true,表示锁已被其他线程占用,线程未能获取到锁。
- 返回旧值:test_and_set 操作会返回标志的旧值。线程可以根据这个返回值判断是否成功获取到锁。如果返回 false,说明成功获取到锁;如果返回 true,则需要等待锁被释放后再次尝试获取。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>std::atomic_flag lock = ATOMIC_FLAG_INIT;// 自旋锁类
class SpinLock {
public:void lock() {// 持续尝试获取锁,直到成功while (lock.test_and_set(std::memory_order_acquire)) {// 自旋等待}}void unlock() {// 释放锁,将标志设置为 falselock.clear(std::memory_order_release);}
};SpinLock spinLock;
int sharedResource = 0;// 线程函数
void worker() {for (int i = 0; i < 100000; ++i) {spinLock.lock();++sharedResource;spinLock.unlock();}
}int main() {std::vector<std::thread> threads;// 创建多个线程for (int i = 0; i < 4; ++i) {threads.emplace_back(worker);}// 等待所有线程完成for (auto& thread : threads) {thread.join();}std::cout << "Shared resource value: " << sharedResource << std::endl;return 0;
}
自旋锁优点:
-
无上下文切换:自旋锁不会引起线程挂起,因此避免了上下文切换的开销。在锁竞争较轻时,自旋锁可以高效地工作。
-
简单高效:实现简单,且不依赖操作系统调度,适合锁竞争不严重的场景。
自旋锁缺点:
-
CPU资源浪费:如果锁被占用,自旋锁会不断地循环检查锁的状态,浪费 CPU 时间,尤其是在锁持有时间较长时,可能导致性能问题。
-
不适合锁竞争场景:当有大量线程竞争同一个锁时,自旋锁的性能将大幅下降,因为大部分时间都在自旋,浪费了 CPU 资源。
自旋锁的适用场景:
-
短时间锁竞争:自旋锁适用于临界区代码执行时间非常短的情况。如果锁持有时间较长,使用自旋锁就不合适了。
-
锁竞争较轻:在多线程程序中,如果线程数量较少且资源竞争较少,自旋锁可以有效减少线程上下文切换,提升性能。
-
实时系统或高性能系统:在某些对延迟非常敏感的应用场景中,自旋锁可以通过减少上下文切换来提供更低的延迟。
总结:自旋锁是一种简单且高效的锁机制,通过原子操作避免了线程上下文切换,适合用于短时间锁竞争和低延迟要求的场景。在锁竞争激烈或锁持有时间较长时,自旋锁的性能会受到影响,这时传统的互斥锁(如 std::mutex
)可能更为合适。
递归锁
在 C++ 中,递归锁也被称为可重入锁,它是一种特殊的锁机制,允许同一个线程多次获取同一把锁而不会产生死锁。
原理
普通的互斥锁(如 std::mutex
)不允许同一个线程在已经持有锁的情况下再次获取该锁,否则会导致死锁。因为当线程第一次获取锁后,锁处于被占用状态,再次尝试获取时,由于锁未被释放,线程会被阻塞,而该线程又因为被阻塞无法释放锁,从而陷入死循环。
递归锁则不同,它内部维护了一个计数器和一个持有锁的线程标识。当一个线程第一次获取递归锁时,计数器加 1,同时记录该线程的标识。如果该线程再次请求获取同一把锁,计数器会继续加 1,而不会被阻塞。当线程释放锁时,计数器减 1,直到计数器为 0 时,锁才会真正被释放,其他线程才可以获取该锁。
应用场景:
- 递归调用:在递归函数中,如果需要对共享资源进行保护,使用递归锁可以避免死锁问题。例如,在一个递归遍历树结构的函数中,可能需要对树节点的某些属性进行修改,此时可以使用递归锁来保证线程安全。
- 嵌套锁:当代码中存在多层嵌套的锁获取操作,且这些操作可能由同一个线程执行时,递归锁可以避免死锁。例如,一个函数内部调用了另一个函数,这两个函数都需要获取同一把锁。
注意事项:
1. 性能开销
递归锁的实现比普通互斥锁更为复杂。普通互斥锁只需简单地标记锁的占用状态,当一个线程请求锁时,检查该状态并进行相应操作。而递归锁除了要维护锁的占用状态,还需要记录持有锁的线程标识以及一个计数器,用于跟踪同一个线程获取锁的次数。每次获取和释放锁时,都需要对这些额外信息进行更新和检查,这无疑增加了系统的开销。
- 时间开销:由于额外的状态检查和更新操作,递归锁的加锁和解锁操作通常比普通互斥锁更耗时。在高并发、对性能要求极高的场景下,频繁使用递归锁可能会成为性能瓶颈。
- 资源开销:记录线程标识和计数器需要额外的内存空间,虽然这部分开销相对较小,但在资源受限的系统中,也可能会产生一定的影响。
建议:在不需要递归获取锁的场景下,应优先使用普通互斥锁(如 std::mutex)。
2. 死锁风险
虽然递归锁允许同一个线程多次获取同一把锁而不会死锁,但如果在递归调用过程中,锁的获取和释放逻辑出现错误,仍然可能导致死锁。例如,在递归函数中,获取锁后在某些条件下没有正确释放锁就进行了递归调用,可能会导致锁无法正常释放,其他线程请求该锁时就会陷入死锁。
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex recMutex;void faultyRecursiveFunction(int n) {if (n == 0) return;std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;if (n == 2) {// 错误:没有释放锁就返回,可能导致死锁return;}faultyRecursiveFunction(n - 1);
}int main() {std::thread t(faultyRecursiveFunction, 3);t.join();return 0;
}
3.不同递归锁之间的交叉锁定
当存在多个递归锁时,如果不同线程以不同的顺序获取这些锁,就可能会产生死锁。例如,线程 A 先获取了递归锁 L1,然后尝试获取递归锁 L2;而线程 B 先获取了递归锁 L2,然后尝试获取递归锁 L1。此时,两个线程都在等待对方释放锁,从而陷入死锁状态。
在 C++ 标准库中,std::recursive_mutex
是递归锁的实现。以下是一个简单的示例代码:
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex recMutex;// 递归函数,多次获取递归锁
void recursiveFunction(int n) {if (n == 0) return;// 加锁std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;// 递归调用recursiveFunction(n - 1);// 锁在离开作用域时自动释放
}int main() {std::thread t(recursiveFunction, 3);t.join();return 0;
}
什么是锁的重入与不可重入?
可重入锁也叫递归锁,允许同一个线程在已经持有该锁的情况下,再次获取同一把锁而不会产生死锁。可重入锁内部会维护一个持有锁的线程标识和一个计数器。当线程第一次获取锁时,会记录该线程的标识,并将计数器初始化为 1。如果该线程再次请求获取同一把锁,锁会检查请求线程的标识是否与当前持有锁的线程标识相同,如果相同,则将计数器加 1,而不会阻塞该线程。释放锁时,计数器减 1,直到计数器为 0 时,锁才会释放,其他线程才可以获取该锁。
不可重入锁不允许同一个线程在已经持有该锁的情况下再次获取同一把锁。如果一个线程已经持有了不可重入锁,再次请求获取该锁时,会导致线程阻塞,进而可能产生死锁。不可重入锁只关注锁的占用状态,不记录持有锁的线程标识和获取锁的次数。当一个线程请求获取锁时,锁会检查其是否已被占用,如果已被占用,无论请求线程是否就是持有锁的线程,都会将该线程阻塞。
相关文章:
常见的“锁”有哪些?
悲观锁 悲观锁认为在并发环境中,数据随时可能被其他线程修改,因此在访问数据之前会先加锁,以防止其他线程对数据进行修改。常见的悲观锁实现有: 1.互斥锁 原理:互斥锁是一种最基本的锁类型,同一时间只允…...
二级公共基础之数据库设计基础(一) 数据库系统的基本概念
目录 前言 一、数据库、数据管理系统和数据库系统 1.数据 2.数据库 3.数据库管理系统 1.数据库管理系统的定义 2.数据库管理系统的功能 1.数据定义功能 2.数据操作功能 3.数据存取控制 4.数据完整性管理 5.数据备份和恢复 6.并发控制 4.数…...

ollama无法通过IP:11434访问
目录 1.介绍 2.直接在ollama的当前命令窗口中修改(法1) 3.更改ollama配置文件(法2) 3.1更新配置 3.2重启服务 1.介绍 ollama下载后默认情况下都是直接在本地的11434端口中运行,绑定到127.0.0.1(localhost)&#x…...
简单易懂,解析Go语言中的struct结构体
目录 4. struct 结构体4.1 初始化4.2 内嵌字段4.3 可见性4.4 方法与函数4.4.1 区别4.4.2 闭包 4.5 Tag 字段标签4.5.1定义4.5.2 Tag规范4.5.3 Tag意义 4. struct 结构体 go的结构体类似于其他语言中的class,主要区别就是go的结构体没有继承这一概念,但可…...

java给钉钉邮箱发送邮件
1.开通POP和IMAP 2.引入pom <dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.7</version> </dependency>3.逻辑 String host "smtp.qiye.aliyun.com"; String port "…...

C++和OpenGL实现3D游戏编程【连载23】——几何着色器和法线可视化
欢迎来到zhooyu的C++和OpenGL游戏专栏,专栏连载的所有精彩内容目录详见下边链接: 🔥C++和OpenGL实现3D游戏编程【总览】 1、本节实现的内容 上一节课,我们在Blend软件中导出经纬球模型时,遇到了经纬球法线导致我们在游戏中模型光照显示问题,我们在Blender软件中可以通过…...

大连本地知识库的搭建--数据收集与预处理_01
1.马蜂窝爬虫 编程语言:Python爬虫框架:Selenium(用于浏览器自动化)解析库:BeautifulSoup(用于解析HTML) 2.爬虫策略 目标网站:马蜂窝(https://www.mafengwo.cn/&…...
github 推送的常见问题以及解决
文章目录 git add 的时候问题1为什么会发生这种情况?Git 的警告含义如何解决?1. **保持 Git 的默认行为(推荐)**2. **禁用自动转换**3. **仅在工作目录中禁用转换**4. **统一使用 LF(跨平台开发推荐)** git…...

stm32单片机个人学习笔记16(SPI通信协议)
前言 本篇文章属于stm32单片机(以下简称单片机)的学习笔记,来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记,只能做参考,细节方面建议观看视频,肯定受益匪浅。 STM32入门教程-2023版 细…...
Linux | RHEL / CentOS 中 YUM history / downgrade 命令回滚操作
注:英文引文,机翻未校。 在 RHEL/CentOS 系统上使用 YUM history 命令回滚升级操作 作者: 2daygeek 译者: LCTT DarkSun 为服务器打补丁是 Linux 系统管理员的一项重要任务,为的是让系统更加稳定,性能更加…...

BGP状态和机制
BGP邻居优化 为了增加稳定性,通常建议实验回环口来建立邻居。更新源:建立邻居和邻居所学习到的路由的下一跳。多跳:EBGP邻居建立默认选哟直连,因为TTL=1,如果非直连,必须修改TTL。命令备注peer 2.2.2.2 connect-interface lo1配置更新源peer 2.2.2.2 ebgp-max-hop 2配置T…...

温湿度监控设备融入智慧物联网
当医院的温湿度监控设备融入智慧物联网,将会带来许多新的体验,可以帮助医院温湿度监控设备智能化管理,实现设备之间的互联互通,方便医院对温湿度数据进行统一管理和分析。 添加智慧物联网技术,实现对医院温湿度的实时…...

smolagents学习笔记系列(五)Tools-in-depth-guide
这篇文章锁定官网教程中的 Tools-in-depth-guide 章节,主要介绍了如何详细构造自己的Tools,在之前的博文 smolagents学习笔记系列(二)Agents - Guided tour 中我初步介绍了下如何将一个函数或一个类声明成 smolagents 的工具&…...
前端面试真题 2025最新版
文章目录 写在前文CSS怪异盒模型JS闭包闭包的形成闭包注意点 CSS选择器及优先级优先级 说说flex布局及相关属性Flex 容器相关属性:Flex 项目相关属性 响应式布局如何实现是否用过tailwindcss,有哪些好处好处缺点 说说对象的 prototype属性及原型说说 pro…...
面试八股文--数据库基础知识总结(1)
1、数据库的定义 数据库(DataBase,DB)简单来说就是数据的集合数据库管理系统(Database Management System,DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。数据库系统…...
10. docker nginx官方镜像使用方法
本文介绍docker nginx官方镜像使用方法,因为第一次用,在加上对docker也不是很熟,中间踩了一些坑,为了避免下一次用又踩坑,因此记录如下,也希望能够帮到其它小伙伴。 官方镜像页面:https://hub.d…...

[Web 安全] PHP 反序列化漏洞 —— PHP 反序列化漏洞演示案例
关注这个专栏的其他相关笔记:[Web 安全] 反序列化漏洞 - 学习笔记-CSDN博客 PHP 反序列化漏洞产生原因 PHP 反序列化漏洞产生的原因就是因为在反序列化过程中,unserialize() 接收的值可控。 0x01:环境搭建 这里笔者是使用 PhpStudy 搭建的环…...

es-head(es库-谷歌浏览器插件)
1.下载es-head插件压缩包,并解压缩 2.谷歌浏览器添加插件 3.使用...
第二十:【路由的props配置】
作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件) {name:xiang,path:detail/:id/:title/:content,component:Detail, 第一种方法:// props的对象写法,作用:把对象中的每一组key-valu…...
Vue 2全屏滚动动画实战:结合fullpage-vue与animate.css打造炫酷H5页面
引言 在移动端H5开发中,全屏滚动效果因其沉浸式体验而广受欢迎。如何快速实现带有动态加载动画的全屏滚动页面?本文将手把手教你使用 Vue 2、全屏滚动插件 fullpage-vue 和动画库 animate.css 3.5.1,打造一个高效且视觉冲击力强的H5页面。通…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...

IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
#Uniapp篇:chrome调试unapp适配
chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器:Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...