锁的本质

锁的本质:操作的序列化、队列化

虽然不同的语言可能会提供不同的锁接口,但是底层调用的都是操作系统的提供的锁。

不同的高级语言只是在操作系统的锁机制基础上进行了些封装而已


硬件

原子操作是指不可被中断的一个或者一组操作。

硬件提供原子指令,支持基本类型。

总线锁

在cpu芯片上有一个HLOCK Pin,可以通过发送指令来操作,将#HLOCK Pin电位拉低,并持续到这条指令执行完毕,从而将总线锁住,这样同一总线上的其他CPU就不能通过总线来访问内存了,保证了这条指令在多处理器环境中的原子性。

总线锁代价高。

缓存锁

某个CPU对缓存数据进行更改时,会通知缓存了该数据的该数据的CPU抛弃缓存的数据或者从内存重新读取。

缓存锁代价低,但是复杂,需要额外的代价保证缓存一致性。

MESI协议是经典的缓存一致性协议。

两种不能使用缓存锁的情况

  • 第一种情况是操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。
  • 第二种情况是处理器不支持缓存锁定,对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

伪共享

如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)。


linux下查看Cache Line大小,一般为64Byte

cat /proc/cpuinfo | grep "cache_alignment"


避免伪共享。对于多线程共享变量才需要加对齐防止伪共享。缓存行填充,将结构体的大小前后凑齐到64Byte。

缓存行对齐。如果两个数据,访问其中一个意味着大概率也会访问另一个,那么可以将它们放到同一个 cacheline。

操作系统

进程执行时需要资源,操作系统做了归一化处理,用锁来代替资源。操作系统就不用关心进程的细节。

操作系统会维护一个“锁”的列表;找到这个锁的对应项,读它的相关信息,再找到申请它的进程队列。

锁的分类

  1. “自旋锁”是一种“申请不到也不知会操作系统”的锁。
  2. 其它锁都是“申请不到就通知操作系统:资源不足,我没法干活了,申请休息”。

资源的分类

  1. 同时只允许一个访问,无论读写。“互斥锁”。
  2. 同时只允许一个修改、但可以允许许多个读取(读取时不得写入)。“读写锁”。

通过不同的锁,进程就可以配合操作系统,做到“既不浪费CPU时间、又尽量把各种资源利用到极致”了。

用户态

悲观锁

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。

悲观锁认为修改资源冲突的概率较高,访问资源前,先要加锁。

并发场景下性能显著下降。

乐观锁

认为冲突的概率较低,先修改资源,再回写资源。回写资源时,判断是否冲突。没有冲突,修改成功。存在冲突,放弃操作或者重试。

全程没有涉及到锁,也叫作无锁编程。适用于多读少写的场景。

可以通过版本号机制和CAS算法实现。

乐观锁缺点:

  • ABA问题。增加预期标志。
  • CAS(Compare and Swap)循环时间长开销大。
  • 只能保证一个共享变量的原子操作。

语言层C++

atomic

C++11 提供了一个原子类型 std::atomic,通过这个原子类型管理的内部变量就可以称之为原子变量,可以给原子类型指定 bool、char、int、long、指针等类型作为模板参数 。

std::atomic保证结果,不保证过程,建议调用高效的方式。

std::atomic::is_lock_free用来判断atomic作用于类型T时是否会使用lockfree。

#include <iostream>
#include <utility>
#include <atomic>
 
struct A { int a[100]; };
struct B { int x, y; };
int main(){
    std::cout << std::boolalpha
              << "std::atomic<A> is lock free? "
              << std::atomic<A>{}.is_lock_free() << '\n'
              << "std::atomic<B> is lock free? "
              << std::atomic<B>{}.is_lock_free() << '\n';
}
Possible output:
std::atomic<A> is lock free? false
std::atomic<B> is lock free? true

如果is_lock_free=true,底层会调用cpu提供的原子指令。否则,调用mutex。

mutex

标准的互斥锁,满足三个1:一个线程,锁一次,解锁一次。

recursive_mutex

可重入互斥锁。满足1NN:一个线程,锁N次,解锁N次。

lock_guard

简单的RAII封装,在构造函数中进行加锁,析构函数中进行解锁

unique_lock

std::lock_guard的功能超集。

性能和内存开销都比std::lock_guard大得多。需要有选择地使用。

condition_variable

条件变量,这个不是锁,是一种线程间的通讯机制,并且几乎总是和互斥量一起使用的。

等待-通知。

std::mutex m;
std::condition_variable cond;
bool data_ready=false;

void process_data();

void foo(){
    std::unique_lock<std::mutex> lk(m);
    cond.wait(lk,[]{return data_ready;});//return only if data_ready is true
    process_data();
}

void set_data_ready(){
    std::lock_guard<std::mutex> lk(m);
    data_ready=true;
    cond.notify_one();
}

refer

https://www.zhihu.com/question/66733477/answer/246760992

posted @   天下太平  阅读(268)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示