Colderleo's blog Colderleo's blog
首页
Linux
C++
Python
前端
工具软件
mysql
索引
关于
GitHub (opens new window)

Colder Leo

热爱代码的打工人
首页
Linux
C++
Python
前端
工具软件
mysql
索引
关于
GitHub (opens new window)
  • bug定位的一些情形
  • c++性能调优,可能的情况
  • total-编程知识点集锦
  • hpc_common.hpp
  • memory order 内存模型
  • 类型推导之auto-template-decltype
  • 完美转发forward源码分析
  • 左值和右值,右值引用、重载 std-move,引用折叠
  • cmake用法
  • alignas、alignof、sizeof实现内存对齐分配
  • 通过宏定义控制debug打印
  • 程序耗时性能测试
  • 线程池开源项目阅读
  • C++类中包含没有默认构造函数的成员
  • C++可变参数模板
  • C++属性继承 public protected private
  • C++智能指针
  • C++导出so的方法,以及extern C 注意事项
  • 四种spin lock
    • 参考
    • naive lock
    • Ticket Lock
    • CLH Lock
    • MCS锁
    • spin lock优化 Pause指令
  • condition_variable和unique_lock
  • dpdk、kernel bypass
  • 智能网卡solarflare、Mellanox、X10等
  • 汇编寄存器和常见指令
  • c++ 类的静态成员变量未定义
  • C++获取类成员函数地址
  • preload示例
  • C++异常安全和RAII
  • C++11单例模式
  • C++歪门邪道
  • boost-program-option用法
  • c++17通过模板获取类成员的个数
  • 通过模板实现结构体成员拷贝-按成员名匹配
  • STL学习路径
  • boost库安装使用方法
  • C++文件读写
  • linux下socket通信demo,server和client
  • makefile写法
  • RxCpp
  • C++
gaoliu
2021-10-23
目录

四种spin lock

# 参考

https://www.cnblogs.com/stevenczp/p/7136416.html (opens new window)

CLH锁 、MCS锁 (opens new window)

基于队列的锁:mcs lock简介 (opens new window)

Spin Lock Performance (opens new window)

如果不用OS提供的mutex,我们该如何实现互斥锁?(不考虑重入的情况)

# naive lock

最简单的想法是,搞一个volatile类型的共享变量flag,值可以是flase(无锁)或者true(有锁),竞争线程监听flag,一旦发现flag为false,那么尝试cas更新flag为true,更新成功则说明占有了这个锁,更新失败说明临界区已经被其他线程占领,继续监听flag并尝试更新。占有锁的线程退出的时候,将flag修改为false,表示释放锁。

    volatile boolean flag = false;

    void lock() {
        while (!cas(flag, false, true)) {//返回true:占锁成功,返回false:占锁失败,继续循环尝试

        }
    }

    void unlock() {
        flag = false;
    }
1
2
3
4
5
6
7
8
9
10
11

这样做有个问题是无法保证公平性,可能有的倒霉蛋空转了一辈子也无法cas成功,无法做到按竞争线程先来后到的次序占有锁。

# Ticket Lock

为了提供公平,有人发明了Ticket Lock

线程想要竞争某个锁,需要先领一张ticket,然后监听flag,发现flag被更新为手上的ticket的值了,才能去占领锁

就像是在医院看病一样,医生就是临界区,病人就是线程,病人挂了号领一张单子,单子上写了一个独一无二的号码,病人等的时候就看屏幕,屏幕上显示到自己的号码了,才能进去找医生。

    AtomicInteger ticket = new AtomicInteger(0);
    volatile int flag = 0;

    void lock() {
        int my_ticket = ticket.getAndIncrement();//发号必须是一个原子操作,不能多个线程拿到同一个ticket
        while (my_ticket != flag) {

        }
    }

    void unlock() {
        flag++;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

现在公平性的问题没有了,但是所有的线程都在监听flag变量,而且由于为了保证flag变量变化的可见性,它必须是volatile的。也就是说如果某个线程修改了flag变量,都会引起其他所有监听线程所在的core的对应于flag变量的cache line被设为invalid,那么这些线程下一次查询flag变量的时候,就必须从主存里取最新的flag数据了,由于主存带宽有限,这个开销较为昂贵(与监听线程数成正比)。

# CLH Lock

为了减少缓存一致性带来的开销,CLH Lock被发明了。

ps,CLH实际上是指三个人:Craig, Landin, and Hagersten

CLH锁的核心思想是,1. 竞争线程排队 2. 监听变量拆分

CLH锁维护了一个链表waitingList的head与tail,其节点定义如下:

    static class Node {
        volatile boolean flag;//true:当前线程正在试图占有锁或者已经占有锁,false:当前线程已经释放锁,下一个线程可以占有锁了
        Node prev;//监听前一个节点的flag字段
    }
1
2
3
4

初始时需要定义一个dummy节点(dummpy.flag == true, dummy.prev == null),head == tail == dummy

当有线程想要获取锁时,先创建一个链表节点node,然后将node挂载在waitingList的尾部(尝试cas(tail, oldTail, node),如果成功将node.prev更新为oldTail,失败则重试)

然后这个线程就监听node.prev.flag,什么时候node.prev.flag == false了,说明node的前一个节点对应的线程已经释放了锁,本线程此时可以安全的占有锁了

释放锁的时候,将对应的node.flag修改为false即可。

实现代码如下(相当粗糙,意会即可):

public class CLHLock {
    volatile Node head, tail;//waitingList

    public CLHLock() {
        head = tail = Node.DUMMY;
    }

    public Node lock() {
        //lock-free的将node添加到waitingList的尾部
        Node node = new Node(true, null);
        Node oldTail = tail;
        while (!cas(tail, oldTail, node)) {
            oldTail = tail;
        }
        node.setPrev(oldTail);

        while (node.getPrev().isLocked()) {//监听前驱节点的locked变量
        }

        return node;
    }

    public void unlock(Node node) {
        node.setLocked(false);
    }

    static class Node {
        public Node(boolean locked, Node prev) {
            this.locked = locked;
            this.prev = prev;
        }

        volatile boolean locked;//true:当前线程正在试图占有锁或者已经占有锁,false:当前线程已经释放锁,下一个线程可以占有锁了
        Node prev;//监听前一个节点的locked字段

        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }

        public Node getPrev() {
            return prev;
        }

        public void setPrev(Node prev) {
            this.prev = prev;
        }

        public static final Node DUMMY = new Node(false, null);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

这样做可以极大的减少缓存一致性协议所带来的开销。

CLH锁的变种被应用于Java J.U.C包下的AbstractQueuedSynchronizer

# MCS锁

CLH锁并不是完美的,因为每个线程都是在前驱节点的locked字段上自旋,而在NUMA体系中,有可能多个线程工作在多个不同的socket上的core里。如果前驱节点的内存跟监听线程的core距离过远,会有性能问题。

于是MCS锁诞生了

ps,MCS也是人名简写:John M. Mellor-Crummey and Michael L. Scott

MCS与CLH最大的不同在于:CLH是在前驱节点的locked域上自旋,MCS是在自己节点上的locked域上自旋。

具体的实现是,前驱节点在释放锁之后,会主动将后继节点的locked域更新。

也就是把多次对远端内存的监听 + 一次对本地内存的更新,简化成了多次对本地内存的监听 + 一次对远端内存的更新。

具体的实现如下

public class MCSLock {
    volatile Node head, tail;//waitingList

    public MCSLock() {
        head = tail = null;
    }

    public Node lock() {
        //lock-free的将node添加到waitingList的尾部
        Node node = new Node(true, null);
        Node oldTail = tail;
        while (!cas(tail, oldTail, node)) {
            oldTail = tail;
        }

        if (null == oldTail) {//如果等待列表为空,那么获取锁成功,直接返回
            return node;
        }

        oldTail.setNext(node);
        while (node.isLocked()) {//监听当前节点的locked变量
        }

        return node;
    }

    public void unlock(Node node) {
        if (node.getNext() == null) {
            if (cas(tail, node, null)) {//即使当前节点的后继为null,也要用cas看一下队列是否真的为空
                return;
            }
            while (node.getNext() != null) {//cas失败,说明有后继节点,只是还没更新前驱节点的next域,等前驱节点看到后继节点后,即可安全更新后继节点的locked域

            }
        }
        node.getNext().setLocked(false);
    }

    static class Node {
        public Node(boolean locked, Node next) {
            this.locked = locked;
            this.next = next;
        }

        volatile boolean locked;//true:当前线程正在试图占有锁或者已经占有锁,false:当前线程已经释放锁,下一个线程可以占有锁了
        Node next;//后继节点

        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# spin lock优化 Pause指令

https://developer.aliyun.com/article/698642?spm=a2c6h.14164896.0.0.1b0a6556pDxFIm (opens new window)

Spin Lock Performance (opens new window)

https://blog.csdn.net/lotluck/article/details/78329162 (opens new window)

_mm_pause();

__asm volatile ("pause" ::: "memory");

__asm__ (".byte 0xf3, 0x90");

__asm__ ("PAUSE")
1
2
3
4
5
6
7

经过上网查找资料 asm (".byte 0xf3, 0x90") intel的pause指令。当spinlock执行lock()获得锁失败后会进行busy loop,不断检测锁状态,尝试获得锁。这么做有一个缺陷:频繁的检测会让流水线上充满了读操作。另外一个线程往流水线上丢入一个锁变量写操作的时候,必须对流水线进行重排,因为CPU必须保证所有读操作读到正确的值。流水线重排十分耗时,影响lock()的性能。

参考这位同学的文章: 自旋锁spinlock剖析与改进

Pause指令解释(from intel): Description Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.

提升spin-wait-loop的性能,当执行spin-wait循环的时候,笨死和小强处理器会因为在退出循环的时候检测到memory order violation而导致严重的性能损失,pause指令就相当于提示处理器哥目前处于spin-wait中。在绝大多数情况下,处理器根据这个提示来避免violation,藉此大幅提高性能,由于这个原因,我们建议在spin-wait中加上一个pause指令。

编辑 (opens new window)
上次更新: 2023/05/07, 17:27:54
C++导出so的方法,以及extern C 注意事项
condition_variable和unique_lock

← C++导出so的方法,以及extern C 注意事项 condition_variable和unique_lock→

最近更新
01
通过模板实现结构体成员拷贝-按成员名匹配
05-07
02
c++17通过模板获取类成员的个数
05-01
03
avx-sse切换惩罚
04-30
更多文章>
Theme by Vdoing | Copyright © 2019-2023 Colder Leo | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×