【高并发基础】基本锁算法及原理

news2024/11/26 19:48:51

系列综述:
💞目的:本系列是个人整理为了秋招面试的,整理期间苛求每个知识点,平衡理解简易度与深入程度。
🥰来源:材料主要源于多处理器编程的艺术进行的,每个知识点的修正和深入主要参考各平台大佬的文章,其中也可能含有少量的个人实验自证。
🤭结语:如果有帮到你的地方,就点个赞关注一下呗,谢谢🎈🎄🌷!!!
🌈【C++】秋招&实习面经汇总篇


文章目录

    • 一、绪论
      • 锁的底层构建
      • 常见基本原子操作指令
    • 二、锁的分类及特点
      • 按加锁失败后的处理方式
        • 自旋锁
        • 互斥锁 / 阻塞锁
      • 是否给同步资源上锁
        • 悲观锁
        • 乐观锁
      • 多线程能否共享一把锁
      • 其他分类
        • 是否可以中断
        • 同一个线程是否可以获取同一把锁
        • 多线程竞争时,是否按序排队
    • 三、锁的应用
    • 参考博客


😊点此到文末惊喜↩︎


一、绪论

锁的底层构建

  1. 并发问题的原因
    • 独立瞬时的读写操作导致写前和读后对于状态的未知性
      • Test读:看一眼就把眼睛闭上,即读只能读到一瞬间的状态,读的是一个history(过去值)
      • Set写:闭着眼睛改变,即写入只能覆盖,无法知道写入时的状态
  2. 基本架构
    • 共享内存的(异步)多核处理器下,并发程序的特点是宏观并行、微观串行
    • 微观串行是指并发程序的对共享的内存单元操作可以形成一个线性的历史操作序列(可线性化)
  3. 寄存器的安全性(这里的寄存器是指内存单元,是并发访问的最小单位,可看作一个D触发器)
    • 安全寄存器safe:顺序计算的单线程程序总是在D触发器的输出电平稳定后才执行下一条指令,所以单线程的读指令可以正确读到最近写的新值。
    • 不安全寄存器unsafe:在多个线程同时访问一个寄存器的情况下,由于线程访问寄存器的时机是无法控制的,会出现某个线程改变了寄存器的值后,在电平还未稳定的情况下,另一个线程就开始读这个寄存器的值。
  4. 寄存器满足的性质划分
    • 安全的safe
      • 读写操作不重叠:读操作可以给出最近的写入值
      • 读写操作重叠:会读出寄存器可表示范围内的任意值。
    • 正则的regular:
      • 读写操作不重叠:读操作可以给出最近的写入值
      • 读写操作重叠:会读出寄存器前一次写操作或当前写操作的值。
    • 原子的atomic:
      • 读写操作不重叠:读操作可以给出最近的写入值
      • 读写操作重叠:当前一个读操作读到新值后,后续的读操作不再被允许读到旧值
  5. 寄存器读写升级
    • 安全布尔单读单写 (SRSW safe Boolean)→ 安全布尔多读单写(MRSW safe Boolean)
      • 原理:写线程依次向所有寄存器数组元素写入新值,读线程读取所对应数组元素的寄存器值
    • 安全布尔多读单写(MRSW safe Boolean) → 正则布尔多读单写(MRSW safe Boolean)
      • 原理:通过预存所写的最近一个值来避免安全寄存器在连续写入A值却返回其他值的随机情况
    • 正则布尔多读单写 → 正则多值多读单写(MRSW regular)
      • 原理:用单读单写寄存器数组构建一个多读单写寄存器
    • 正则单读单写 → 原子单读单写
      • 原理:使用新旧值的时间戳保证正则寄存器读写重叠的顺序(可线性化)
    • 原子单读单写 → 原子多读单写
      • 原理:扩充成二维数组。每个读操作读取自己对应索引的整个行,然后比较哪个是最新的,返回最新的值。写操作写入自己对应的整个列,保证后续的读操作在其自己的行内一定能读到最近写操作的值。
    • 原子多读单写 → 原子多读多写
      • 原理1:每个写线程在写入值之前先读取寄存器数组的所有时间戳,然后选出最大值并加 1 作为本次写操作的时间戳,写入本线程对应的数组位置。如果两个写线程写入的时间戳大小相同,那么可以按照字典排序的方法确定两个时间戳的相对大小。
      • 原理2:通过简单快照,比较前后两次收集信息是否相同,来判断是否出现更新。但可能出现ABA问题,所以使用带时间戳的简单快照,可以判断两次收集的信息是否相同。但 收集(scan) 期间如果被连续打断,会导致无法scan完成。无等待快照,使用助人为乐机制,写线程会帮助其他被中断的线程完成scan操作。

常见基本原子操作指令

  1. TAS(test-and-set)
    • 原理:读取旧值,写入新值,返回旧值
    • 优点
      • TAS指令操作的原子性是硬件保证的
    • 缺点:
      • 可拓展性差:TAS锁的延迟却随着线程数的增加而急剧增长,TTAS可以提升TSA的延展性,但仍有限。
    • 代码
    // 使用TAS实现自旋锁
    bool val = false;// 初始化共享变量
    // 原子操作指令
    bool TestAndSet(bool new_val) {
    	bool prior_val = val;	// 读取旧值
    	val = new_val;			// 设置新值
    	return prior_val;		// 返回新值
    }
    // 自旋锁
    void lock() {// TAS形式
    	// 一直尝试上锁,当返回值为false停止自旋(while循环),执行临界区代码
    	while (getAndSet(true)) {} 
    }
    
    void lock() {// TTAS形式
     while (true) {
    	 while (get()) {} //空转读,直到锁被释放
    	 if (TestAndSet(true))
    	 return; //加锁成功
    }
    void unlock() {
    	// 临界区结束释放锁,其他线程运行加锁时,↓
    	// getAndSet会返回false,中断自旋而获得锁运行。
    	getAndSet(false);
    }
    
  2. CAS(compare-and-set)
    • 原理:当且仅当预期原值E内存值V相同时,将内存值V修改为新值N,否则什么都不做
    • 优点:CAS可以支持任意有穷线程的并发
    • 缺点:
      • ABA问题:若共享资源从A状态变为B状态,然后又变回A状态,则CAS会认为没有发生改变,可以使用带时间戳的CAS进行区分相同值的新旧状态
      • 竞争消耗:若线程竞争大,自旋的比较尝试会对CPU的消耗量比较大
      • 多变量原子性:只能保证一个共享变量的原子操作,通过封装成锁可以保证多变量的原子性
    • 代码
    bool CompareAndSet(int expected, int new_val) {
    	int prior = value;			// 读取旧值
    	if (value == expected) {	// 将旧值与预测值比较
    		value = new_val;		// 写入新值
    		return true;			// 写入成功
    	}
    	return false;				// 其他情况表示写入失败
    }
    
  3. get-and-increment(原子增加)
  4. get-and-decrement(原子减少)

二、锁的分类及特点

在这里插入图片描述

按加锁失败后的处理方式

自旋锁

  1. 定义:自旋锁(spinlock)加锁失败后,线程会忙等待,直到获取到锁。加锁开销小,适合锁保护的临界区短锁竞争不激烈的情况。
    • 忙等待:获取锁失败的线程会一直执行Test操作(查看锁状态),但是不执行其他有效工作。
    • 加锁开销小:上锁只需要执行一条原子交换指令,并且没有线程切换开销。
    • 适合情况:锁保护的临界区短,或竞争不激烈,表示其他线程忙等的概率较低,所以在这种情况下,性能较高。
  2. 特点
    • 快的时候很快:只需要一条原子指令的开销即可获得锁
    • 慢的时候很慢:在时间片轮转算法下,若获得自旋锁的线程切换或睡眠,而其他线程获得CPU会空转,能实现了100%的资源浪费
  3. TAS和CAS实现自旋锁的比较(先思后做,因为调研思考的开销小,而做的开销大)
    • TAS差的原因:每次获取锁都会使用原子操作修改标志位,并在总线上发送广播消息,使得其他CPU缓存中的标志位失效,这会导致总线流量增加,缓存命中率降低,以及其他线程的指令延迟。
    • CAS好的原因:只有在预期值与当前值相等时才会使用原子操作修改当前值,并在总线上发送广播消息,否则只会读取当前值,并不会修改它,这会减少总线流量和缓存失效的次数,提高性能。
  4. 自旋锁示例
    int xchg(volatile int *addr,int newval){
      int res;
      asm volatile (	// 内联汇编声明:volatile表示不允许编译器优化
      "lock xchg %0,%1"	// 汇编模板代码:lock表示锁定内存总线,xhcg原子交换两个数的值
      :"+m"(*addr),"=a"(res) // 输出操作数%0:+m表示操作内存地址*addr,=a表示将内存值赋值给res
      :"1"(newval)		// 输入操作数%1:将newval作为输入操作数,输入到%0中
      :"cc");			// 其他:破坏描述符cc告诉编译器修改了条件码寄存器
      return res;
    }
    // TODO:lock xchg指令访问了内存,为什么不会发生用户态到内核态的切换,因为有硬件对于原子指令的支持?
    // TODO:cmpxchg实现cas的汇编锁写法
    int locked = 0;// 初始化
    // 加锁:每个进程都在自己的时间片不停的自旋尝试
    void spin_lock(){
      while (xchg(&locked,1));// 直到持锁进程释放锁,即将*addr置为0,结束自旋
    }
    void spin_unlock(){
      xchg(&locked,0);
    }
    
    

互斥锁 / 阻塞锁

  1. 定义:互斥锁(mutex)加锁失败后,线程会被阻塞,让出cpu资源给其他线程。使用开销较大,适合锁保护的临界区较长的情况。
    • 阻塞:互斥锁加锁失败后,会陷入内核态阻塞线程,并切换其他线程运行。当持锁线程执行解锁操作时,也会陷入内核态,解锁并唤醒正在等待该锁的线程。
    • 使用开销大:用户态下,在上锁和释放锁的过程中,会发生两次上下文切换
    • 适合情况:锁保护的临界区执行开销比较大,其他线程不会忙等待而造成cpu资源的浪费,但是如果临界区开销比较小,那么互斥锁的开销会占比比较大,不如使用自旋锁。
  2. 对自旋锁进行吸优去慢(工程上的优化,通常只优化常见的80%或者最短的木桶板)
    • Fast path:一条原子指令,上锁成功立即返回,执行结束唤醒阻塞的进程
    • Slow path:上锁失败,立即执行系统调用,阻塞进程,从而减少cpu资源的占用
    • 缺点:阻塞操作需要内核态进行,所以用户态下的自旋锁性能较差
  3. 互斥锁的执行
    #include <pthread.h>
    #include <stdio.h>
    
    // 声明一个互斥锁变量,并静态初始化为默认属性
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    void *thread_function(void *arg) {
        // 获取锁
        pthread_mutex_lock(&mutex);		// 没获取到就阻塞
        // 临界区代码
        TODO;
        // 释放锁
        pthread_mutex_unlock(&lmutex);	// 释放后唤醒阻塞线程
    }
    	
    // 主函数
    int main() {
      // 定义两个线程ID
      pthread_t tid1, tid2;
      // 创建两个线程
      pthread_create(&tid1, NULL, increase, (void*)1);
      pthread_create(&tid2, NULL, increase, (void*)2);
      // 等待两个线程结束
      pthread_join(tid1, NULL);
      pthread_join(tid2, NULL);
      // 销毁互斥锁
      pthread_mutex_destroy(&mutex);
      return 0;
    }
    

是否给同步资源上锁

悲观锁

  1. 定义:悲观锁认为共享资源的竞争比较激烈,所以访问共享资源前要先上锁
  2. 特点:悲观锁的特点是先获取锁再进行业务操作,可以避免脏读和不可重复读的问题,但是会降低并发性和性能,可能造成死锁和阻塞。
  3. 使用场景:悲观锁适合于写入操作比较频繁的场景,因为它可以避免频繁的重试和回滚,保证数据的一致性和安全性。

乐观锁

  1. 定义:乐观锁认为数据一般不会发生冲突,所以不需要加锁,但是需要在更新时判断数据是否被修改过,通常使用带时间戳的CAS算法来实现。
  2. 适合场合:乐观锁适合于读取操作比较频繁的场景,因为它可以减少加锁的开销,提高系统的并发性和吞吐量。

多线程能否共享一把锁

  1. 定义:读写锁(read-write lock)是一种同步机制,用于解决多个线程同时对同一资源进行读或写操作的问题。
    • 读锁/共享锁(shared lock):允许多个线程同时对共享资源进行读操作,但不允许写操作
    • 写锁/排他锁(exclusive lock):只允许一个线程对共享资源进行写操作,但不允许其他线程进
    • 规则:读读不互斥,读写互斥,写写互斥
  2. 对于共享资源的互斥原理
    • 写锁是独占锁:共享资源加写锁后,其他线程无法进行读写操作
    • 读锁是共享锁:共享资源加读锁后,其他线程无法进行写操作,但可以加读锁,进行读操作。
  3. 类型
    • 读优先
      • 原理:当有线程持有读锁时,其他线程可以继续加读锁,但不能加写锁;当没有线程持有任何锁时,如果有线程请求加读锁和写锁,那么优先满足加读锁的请求。
      • 优点:适合于读操作远多于写操作的场景,可以提高系统的吞吐量和响应速度。
      • 缺点:可能导致写饥饿
    • 写优先
      • 原理:当有线程持有读锁时,其他线程不能加读锁,也不能加写锁;当没有线程持有任何锁时,如果有线程请求加读锁和写锁,那么优先满足加写锁的请求。(读读互斥?)
      • 优点:适合于写操作远多于读操作的场景,可以提高系统的实时性和一致性。
      • 缺点:写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被「饿死」
    • 公平读写锁
      • 用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
  4. 适合场景:任何操作读写锁适应于能够明确区分读操作和写操作的场景

其他分类

是否可以中断

  1. 可中断锁
    • 概述:在等待获取锁的过程中能够响应中断请求
    • 优点:可以避免死锁或饿死
    • 缺点:在临界区执行时发生中断可能导致的数据不一致或错误,通常需要进行回滚操作
  2. 不可中断锁
    • 概述:非可中断锁则会忽略中断请求,一直等待直到获取锁或者超时
    • 优缺点与上面相反

同一个线程是否可以获取同一把锁

  1. 可重入锁
    • 概述:允许同一个线程在已经获取锁的情况下,再次获取同一个锁,而不会发生死锁
    • 原理:在获取锁时判断是否是已经持有该锁的线程,如果是,则增加持有次数;在释放锁时判断是否持有次数为零,如果是,则释放该锁。(类似shared_ptr)
    • 优点:可以避免一些死锁情况,如调用的两个同步方法都使用的同一把锁
    • 缺点:维护额外的状态信息会增加开销
  2. 不可重入锁
    • 概述:如果同一个线程已经获取锁,再次请求锁时,会被阻塞,导致死锁。
    • 原理:获取时锁状态,锁被占用则等待,释放时判断锁状态,锁未释放则释放
    • 优缺点与上面相反

多线程竞争时,是否按序排队

  1. 公平锁
    • 概述:多个线程按申请锁的先后顺序获取锁,先判断自己是否为等待队列队首,如果是则尝试抢占锁,如果不是则加入等待队列队尾
    • 优点:所有线程都有机会获取锁,避免饥饿现象
    • 缺点:维护有序等待队列具有性能开销
  2. 非公平锁
    • 概述:多个线程不按照申请顺序获取锁,而是先尝试抢占锁,如果失败,再进入等待队列
    • 优点:减少线程切换开销,提高系统吞吐量
    • 缺点:可能导致某些线程饥饿

三、锁的应用


少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。
不如点赞·收藏·关注一波


🚩点此跳转到首行↩︎

参考博客

  1. 南京大学jyy老师操作系统
  2. 小林coding图解锁算法
  3. 深入理解互斥锁 自旋锁的实现机制
  4. 锁的分类
  5. 待定引用
  6. 待定引用
  7. 待定引用
  8. 待定引用

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

elasticsearch操作(命令方式)

说明&#xff1a;elasticsearch是目前最流行的搜索引擎&#xff0c;功能强大&#xff0c;非常受欢迎。特点是倒排索引&#xff0c;在海量数据的模糊查找领域&#xff0c;效率非常高。elasticsearch的安装和使用参考&#xff1a;http://t.csdn.cn/yJdZb。 本文介绍在es的索引库…

Blender自动化脚本,无人值守批量渲图/渲视频

渲染视频是个非常耗时的大工程&#xff0c;如果要渲染多个视频或者每个视频还需要切换不同的贴图、颜色等&#xff0c;工作量就更离谱了&#xff0c;所以不得不用脚本实现自动化。 Blender的脚本是用Python编写&#xff0c;比PS的js要方便很多。再下载一套Blender对应版本的AP…

十分钟掌握 Vim 编辑器核心功能

十分钟掌握 Vim 编辑器核心功能 文章目录 十分钟掌握 Vim 编辑器核心功能&#x1f468;‍&#x1f3eb;内容一&#xff1a;前言【Vim是什么】&#x1f468;‍&#x1f52c;内容二&#xff1a;Vim 常用模式&#x1f468;‍&#x1f680;内容三&#xff1a;基本操作&#x1f468;…

三十二章:Progressive Semantic Segmentation ——渐进式语义分割

0.摘要 这项工作的目标是在不过载GPU内存使用或丢失输出分割图中的细节的情况下对高分辨率图像进行分割。内存限制意味着我们必须对大图像进行降采样&#xff0c;或者将图像分为局部补丁进行分离处理。然而&#xff0c;前一种方法会丢失细节&#xff0c;而后一种方法由于缺乏全…

前端学习——Vue (Day1)

Vue 快速上手 Vue 是什么 创建 Vue 实例 Vue2官网&#xff1a;https://v2.cn.vuejs.org/ <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge…

Nexus3部署、配置+SpringBoot项目Demo

Docker部署Nexus 搜索Nexus3镜像&#xff1a;[rootlocalhost ~]# docker search nexus 拉取Nexus3镜像&#xff1a;[rootlocalhost ~]# docker pull sonatype/nexus3 启动Nexus3前查看虚拟机端口是否被占用&#xff1a;[rootlocalhost ~]# netstat -nultp 通过Docker Hub查看安…

MySQL——备份恢复

数据库备份&#xff0c;数据库为school&#xff0c;素材如下 1.创建student和score表 CREATE TABLE student ( id INT(10) NOT NULL UNIQUE PRIMARY KEY , name VARCHAR(20) NOT NULL , sex VARCHAR(4) , birth YEAR, department VARCHAR(20) , address …

【unity3D】水平方向上UI自动排列整齐(Horizontal Layout Group组件)

&#x1f497; 未来的游戏开发程序媛&#xff0c;现在的努力学习菜鸡 &#x1f4a6;本专栏是我关于游戏开发的学习笔记 &#x1f236;本篇是unity的Horizontal Layout Group Horizontal Layout Group 属性介绍属性详解使用以及效果展示补充 属性介绍 属性功能padding布局组边缘…

C语言——qsort函数的使用(详解)

qsort函数详解 前言&#xff1a;一、qsort函数的含义1.1 函数的参数1.2 参数的含义 二、用不同类型数据&#xff0c;测试sqort2.1 对数组内整数进行排序2.2对数组内浮点数进行排序2.3对字符串进行排序2.4对结构体进行排序 三、模拟实现qsort函数 前言&#xff1a; qsort&#…

RK3588平台开发系列讲解(Camera篇)V4L2 接口查询设备能力

文章目录 一、查询设备的基本信息二、查询设备支持的视频格式三、查询支持分辨率四、查询支持的帧率范围沉淀、分享、成长,让自己和他人都能有所收获!😄 📢在使用 V4L2 进行视频采集前,需要先通过查询设备能力来获取设备可以提供的视频格式、分辨率等信息。 一、查询设…

springboot 配置Knife4j- Swagger3.0

一、导入maven包 <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.2</version></dependency> 二、配置config-swagger工具 package com.exceltotxt.…

MySQL连接查询与存储过程

一、连接查询1.1 内连接1.2 左连接1.3 右连接 二、存储过程2.1 概述2.2 简介2.3 优点2.4 语法2.5 举例2.5.1 创建存储过程2.5.2 调用存储过程2.5.3 查看存储过程2.5.4 存储过程的参数2.5.5 修改存储过程2.5.6 删除存储过程 三、总结 一、连接查询 MySQL 的连接查询&#xff0c…

Java集合是Set

HashSet集合 HashSet集合的特点 HashSet常用方法 ①&#xff1a;add(Object o)&#xff1a;向Set集合中添加元素&#xff0c;不允许添加重复数据。 ②&#xff1a;size()&#xff1a;返回Set集合中的元素个数 public class Test {public static void main(String[] args) {…

LeetCode107. 二叉树的层序遍历 II

107. 二叉树的层序遍历 II 文章目录 [107. 二叉树的层序遍历 II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/)一、题目二、题解方法一&#xff1a;正常层序遍历翻转数组 一、题目 给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序…

HTTP Header定制,客户端使用Request,服务器端使用Response

在服务器端通过request.getHeaders()是无效的&#xff0c;只能使用response.getHeaders()。 Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType mediaType,Class selectedConverterType, ServerHttpRequest request, ServerHttpRespo…

前端将css.html.js打包到一起在手机打开

过程我是按照下面的执行的&#xff0c;大家可以直接参考这个博客里的过程&#xff0c;下面我记录一下遇到的一些问题&#xff0c;我的电脑是mac 打包教程 1.执行命令npm install electron 在安装Electron时报错command sh -c node install.js 在指令后面添加 --ignore-scripts…

Linux 系统中异常与中断

文章目录 异常与中断的关系中断的处理流程异常向量表Linux 系统对中断的处理ARM 处理器程序运行的过程程序被中断时&#xff0c;怎么保存现场Linux 系统对中断处理的演进Linux 对中断的扩展&#xff1a;硬件中断、软件中断硬件中断软件中断 中断处理原则&#xff1a;耗时中断的…

前端学习——ajax (Day3)

AJAX原理 - XMLHttpRequest 使用 XMLHttpRequest <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport&…

JVM运行时数据区——堆内的区域分布

1.堆内的区域分布 堆是运行时数据区最大的一块区域&#xff0c;主要用来存放对象&#xff0c;堆是所有线程公用的&#xff0c;在JVM启动时就被创建&#xff0c;堆的空间是可以调整的&#xff0c;是GC(垃圾回收)的重点区域。 堆的内存空间分区&#xff1a;新生代老年代 新生代…

机器学习 day30(正则化参数λ对模型的影响)

λ对Jcv和Jtrain的影响 假设该模型为四阶多项式当λ很大时&#xff0c;w会很小且接近0&#xff0c;而模型f(x)近似于一个常数&#xff0c;所以此时模型欠拟合&#xff0c;Jtrain和Jcv都很大当λ很小时&#xff0c;模型几乎没有正则化&#xff0c;而四阶多项式的模型十分弯曲&…