锁开销优化以及 CAS 简单说明
一、锁
互斥锁是用来保护一个临界区,即保护一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。
二、锁的开销
在谈及锁的性能开销,一般都会说锁的开销很大,那锁的开销有多大,主要耗在哪,怎么提高锁的性能。
现在锁的机制一般使用 futex(fast Userspace mutexes),内核态和用户态的混合机制。
还没有futex的时候,内核是如何维护同步与互斥的呢?
系统内核维护一个对象,这个对象对所有进程可见,这个对象是用来管理互斥锁并且通知阻塞的进程。如果进程A要进入临界区,先去内核查看这个对象,有没有别的进程在占用这个临界区,出临界区的时候,也去内核查看这个对象,有没有别的进程在等待进入临界区,然后根据一定的策略唤醒等待的进程。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生。
Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。
mutex 是在 futex 的基础上用的内存共享变量来实现的,如果共享变量建立在进程内,它就是一个线程锁,如果它建立在进程间共享内存上,那么它是一个进程锁。pthread_mutex_t 中的 _lock 字段用于标记占用情况,先使用CAS判断_lock是否占用,若未占用,直接返回。否则,通过__lll_lock_wait_private 调用SYS_futex 系统调用迫使线程进入沉睡。 CAS是用户态的 CPU 指令,若无竞争,简单修改锁状态即返回,非常高效,只有发现竞争,才通过系统调用陷入内核态。所以,FUTEX是一种用户态和内核态混合的同步机制,它保证了低竞争情况下的锁获取效率。
所以如果锁不存在冲突,每次获得锁和释放锁的处理器开销仅仅是CAS指令的开销。
确定一件事情最好的方法是实际测试和观测它,让我们写一段代码来测试无冲突时锁的开销:
1 #include <pthread.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 #include <time.h>
5
6 static inline long long unsigned time_ns(struct timespec* const ts) {
7 if (clock_gettime(CLOCK_REALTIME, ts)) {
8 exit(1);
9 }
10 return ((long long unsigned) ts->tv_sec) * 1000000000LLU
11 + (long long unsigned) ts->tv_nsec;
12 }
13
14
15 int main()
16 {
17 int res = -1;
18 pthread_mutex_t mutex;
19
20 //初始化互斥量,使用默认的互斥量属性
21 res = pthread_mutex_init(&mutex, NULL);
22 if(res != 0)
23 {
24 perror("pthread_mutex_init failed\n");
25 exit(EXIT_FAILURE);
26 }
27
28 long MAX = 1000000000;
29 long c = 0;
30 struct timespec ts;
31
32 const long long unsigned start_ns = time_ns(&ts);
33
34 while(c < MAX)
35 {
36 pthread_mutex_lock(&mutex);
37 c = c + 1;
38 pthread_mutex_unlock(&mutex);
39 }
40
41 const long long unsigned delta = time_ns(&ts) - start_ns;
42
43 printf("%f\n", delta/(double)MAX);
44
45 return 0;
46 }
说明:以下性能测试在腾讯云 Intel(R) Xeon(R) CPU E5-26xx v4 1核 2399.996MHz 下进行。
运行了 10 亿次,平摊到每次加锁/解锁操作大概是 2.2ns 每次加锁/解锁(扣除了循环耗时 2.7ns)
在锁冲突的情况下,开销就没有这么小了。
首先pthread_mutex_lock会真正的调用sys_futex来进入内核来试图加锁,被锁住以后线程会进入睡眠,这带来了上下文切换和线程调度的开销。
可以写两个互相解锁的线程来测试这个过程的开销:
1 // Copyright (C) 2010 Benoit Sigoure
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 #include <pthread.h>
17 #include <sched.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <sys/ipc.h>
21 #include <sys/shm.h>
22 #include <sys/syscall.h>
23 #include <sys/wait.h>
24 #include <time.h>
25 #include <unistd.h>
26
27 #include <linux/futex.h>
28
29 static inline long long unsigned time_ns(struct timespec* const ts) {
30 if (clock_gettime(CLOCK_REALTIME, ts)) {
31 exit(1);
32 }
33 return ((long long unsigned) ts->tv_sec) * 1000000000LLU
34 + (long long unsigned) ts->tv_nsec;
35 }
36
37 static const int iterations = 500000;
38
39 static void* thread(void* restrict ftx) {
40 int* futex = (int*) ftx;
41 for (int i = 0; i < iterations; i++) {
42 sched_yield();
43 while (syscall(SYS_futex, futex, FUTEX_WAIT, 0xA, NULL, NULL, 42)) {
44 // retry
45 sched_yield();
46 }
47 *futex = 0xB;
48 while (!syscall(SYS_futex, futex, FUTEX_WAKE, 1, NULL, NULL, 42)) {
49 // retry
50 sched_yield();
51 }
52 }
53 return NULL;
54 }
55
56 int main(void) {
57 struct timespec ts;
58 const int shm_id = shmget(IPC_PRIVATE, sizeof (int), IPC_CREAT | 0666);
59 int* futex = shmat(shm_id, NULL, 0);
60 pthread_t thd;
61 if (pthread_create(&thd, NULL, thread, futex)) {
62 return 1;
63 }
64 *futex = 0xA;
65
66 const long long unsigned start_ns = time_ns(&ts);
67 for (int i = 0; i < iterations; i++) {
68 *futex = 0xA;
69 while (!syscall(SYS_futex, futex, FUTEX_WAKE, 1, NULL, NULL, 42)) {
70 // retry
71 sched_yield();
72 }
73 sched_yield();
74 while (syscall(SYS_futex, futex, FUTEX_WAIT, 0xB, NULL, NULL, 42)) {
75 // retry
76 sched_yield();
77 }
78 }
79 const long long unsigned delta = time_ns(&ts) - start_ns;
80
81 const int nswitches = iterations << 2;
82 printf("%i thread context switches in %lluns (%.1fns/ctxsw)\n",
83 nswitches, delta, (delta / (float) nswitches));
84 wait(futex);
85 return 0;
86 }
编译使用 gcc -std=gnu99 -pthread context_switch.c。
运行的结果是 2003.4ns/ctxsw,所以锁冲突的开销大概是不冲突开销的 910 倍了,相差出乎意料的大。
另外一个c程序可以用来测试“纯上下文切换”的开销,线程只是使用sched_yield来放弃处理器,并不进入睡眠。
1 // Copyright (C) 2010 Benoit Sigoure
2 //
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 #include <sched.h>
17 #include <pthread.h>
18 #include <unistd.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <time.h>
22 #include <string.h>
23 #include <errno.h>
24
25 static inline long long unsigned time_ns(struct timespec* const ts) {
26 if (clock_gettime(CLOCK_REALTIME, ts)) {
27 exit(1);
28 }
29 return ((long long unsigned) ts->tv_sec) * 1000000000LLU
30 + (long long unsigned) ts->tv_nsec;
31 }
32
33 static const int iterations = 500000;
34
35 static void* thread(void*ctx) {
36 (void)ctx;
37 for (int i = 0; i < iterations; i++)
38 sched_yield();
39 return NULL;
40 }
41
42 int main(void) {
43 struct sched_param param;
44 param.sched_priority = 1;
45 if (sched_setscheduler(getpid(), SCHED_FIFO, ¶m))
46 fprintf(stderr, "sched_setscheduler(): %s\n", strerror(errno));
47
48 struct timespec ts;
49 pthread_t thd;
50 if (pthread_create(&thd, NULL, thread, NULL)) {
51 return 1;
52 }
53
54 long long unsigned start_ns = time_ns(&ts);
55 for (int i = 0; i < iterations; i++)
56 sched_yield();
57 long long unsigned delta = time_ns(&ts) - start_ns;
58
59 const int nswitches = iterations << 2;
60 printf("%i thread context switches in %lluns (%.1fns/ctxsw)\n",
61 nswitches, delta, (delta / (float) nswitches));
62 return 0;
63 }
“纯上下文切换” 消耗了大概381.2ns/ctxsw。
这样我们大致可以把锁冲突的开销分成三部分,“纯上下文切换”开销,大概是 381.2ns,调度器开销(把线程从睡眠变成就绪或者反过来)大概是1622.2ns,在多核系统上,还存在跨处理器调度的开销,那部分开销很大。在真实的应用场景里,还要考虑上下文切换带来的cache不命中和TLB不命中的开销,开销只会进一步加大。
三、锁的优化
从上面可以知道,真正消耗时间的不是上锁的次数,而是锁冲突的次数。减少锁冲突的次数才是提升性能的关键。
3.1 避免使用锁
为了提高程序的并行性,最好的办法自然是不使用锁。从设计角度上来讲,锁的使用无非是为了保护共享资源。如果我们可以避免使用共享资源的话那自然就避免了锁竞争造成的性能损失。幸运的是,在很多情况下我们都可以通过资源复制的方法让每个线程都拥有一份该资源的副本,从而避免资源的共享。如果有需要的话,我们也可以让每个线程先访问自己的资源副本,只在程序的最后各个线程的资源副本合并成一个共享资源。例如,如果我们需要在多线程程序中使用计数器,那么我们可以让每个线程先维护一个自己的计数器,只在程序的最后将各个计数器两两归并(类比二叉树),从而最大程度提高并行度,减少锁竞争。
3.2 使用读写锁
如果对共享资源的访问多数为读操作,少数为写操作,而且写操作的时间非常短,我们就可以考虑使用读写锁来减少锁竞争。读写锁的基本原则是同一时刻多个读线程可以同时拥有读者锁并进行读操作;另一方面,同一时刻只有一个写进程可以拥有写者锁并进行写操作。读者锁和写者锁各自维护一份等待队列。当拥有写者锁的写进程释放写者锁时,所有正处于读者锁等待队列里的读线程全部被唤醒并被授予读者锁以进行读操作;当这些读线程完成读操作并释放读者锁时,写者锁中的第一个写进程被唤醒并被授予写者锁以进行写操作,如此反复。换句话说,多个读线程和一个写线程将交替拥有读写锁以完成相应操作。这里需要额外补充的一点是锁的公平调度问题。例如,如果在写者锁等待队列中有一个或多个写线程正在等待获得写者锁时,新加入的读线程会被放入读者锁的等待队列。这是因为,尽管这个新加入的读线程能与正在进行读操作的那些读线程并发读取共享资源,但是也不能赋予他们读权限,这样就防止了写线程被新到来的读线程无休止的阻塞。
需要注意的是,并不是所有的场合读写锁都具备更好的性能,大家应该根据Profling的测试结果来判断使用读写锁是否能真的提高性能,特别是要注意写操作虽然很少但很耗时的情况。
3.3 保护数据而不是操作
在实际程序中,有不少程序员在使用锁时图方便而把一些不必要的操作放在临界区中。例如,如果需要对一个共享数据结构进行删除和销毁操作,我们只需要把删除操作放在临界区中即可,资源销毁操作完全可以在临界区之外单独进行,以此增加并行度。
正是因为临界区的执行时间大大影响了并行程序的整体性能,我们必须尽量少在临界区中做耗时的操作,例如函数调用,数据查询,I/O操作等。简而言之,我们需要保护的只是那些共享资源,而不是对这些共享资源的操作,尽可能的把对共享资源的操作放到临界区之外执行有助于减少锁竞争带来的性能损失。
3.4 粗粒度锁与细粒度锁
使用更细粒度的锁,可以减少锁冲突。这里说的粒度包括时间和空间,比如哈希表包含一系列哈希桶,为每个桶设置一把锁,空间粒度就会小很多--哈希值相互不冲突的访问不会导致锁冲突,这比为整个哈希表维护一把锁的冲突机率低很多。减少时间粒度也很容易理解,加锁的范围只包含必要的代码段,尽量缩短获得锁到释放锁之间的时间,最重要的是,绝对不要在锁中进行任何可能会阻塞的操作。
为了减少串行部分的执行时间,我们可以通过把单个锁拆成多个锁的办法来较小临界区的执行时间,从而降低锁竞争的性能损耗,即把“粗粒度锁”转换成“细粒度锁”。但是,细粒度锁并不一定更好。这是因为粗粒度锁编程简单,不易出现死锁等Bug,而细粒度锁编程复杂,容易出错;而且锁的使用是有开销的(例如一个加锁操作一般需要100个CPU时钟周期),使用多个细粒度的锁无疑会增加加锁解锁操作的开销。在实际编程中,我们往往需要从编程复杂度、性能等多个方面来权衡自己的设计方案。事实上,在计算机系统设计领域,没有哪种设计是没有缺点的,只有仔细权衡不同方案的利弊才能得到最适合自己当前需求的解决办法。例如,Linux内核在初期使用了Big Kernel Lock(粗粒度锁)来实现并行化。从性能上来讲,使用一个大锁把所有操作都保护起来无疑带来了很大的性能损失,但是它却极大的简化了并行整个内核的难度。当然,随着Linux内核的发展,Big Kernel Lock已经逐渐消失并被细粒度锁而取代,以取得更好的性能。
3.5 锁自身的优化
锁本身的行为也存在进一步优化的可能性,sys_futex系统调用的作用在于让被锁住的当前线程睡眠,让出处理器供其它线程使用,既然这个过程的消耗很高,也就是说如果被锁定的时间不超过这个数值的话,根本没有必要进内核加锁——释放的处理器时间还不够消耗的。sys_futex的时间消耗够跑很多次 CAS 的,也就是说,对于一个锁冲突比较频繁而且平均锁定时间比较短的系统,一个值得考虑的优化方式是先循环调用 CAS 来尝试获得锁(这个操作也被称作自旋锁,在glibc库中的互斥锁类型中的适配锁就是这种方式),在若干次失败后再进入内核真正加锁。当然这个优化只能在多处理器的系统里起作用(得有另一个处理器来解锁,否则自旋锁无意义)。在glibc的pthread实现里,通过对pthread_mutex设置PTHREAD_MUTEX_ADAPTIVE_NP属性就可以使用这个机制。
3.6 尽量使用轻量级的原子操作
在例3中,我们使用了mutex锁来保护counter++操作。实际上,counter++操作完全可以使用更轻量级的原子操作来实现,根本不需要使用mutex锁这样相对较昂贵的机制来实现。在今年程序员第四期的《volatile与多线程的那些事儿》中我们就有对Java和C/C++中的原子操作做过相应的介绍。
3.7 使用无锁算法、数据结构
首先要强调的是,笔者并不推荐大家自己去实现无锁算法。为什么别去造无锁算法的轮子呢?因为高性能无锁算法的正确实现实在是太难了。有多难呢?Doug Lea提到java.util.concurrent库中一个Non Blocking的算法的实现大概需要1个人年,总共约500行代码。事实上,我推荐大家直接去使用一些并行库中已经实现好了的无锁算法、无锁数据结构,以提高并行程序的性能。典型的无锁算法的库有java.util.concurrent,Intel TBB等,它们都提供了诸如Non-blocking concurrent queue之类的数据结构以供使用。
四、CAS
锁产生的一些问题:
- 等待互斥锁会消耗宝贵的时间,锁的开销很大。
- 低优先级的线程可以获得互斥锁,因此阻碍需要同一互斥锁的高优先级线程。这个问题称为优先级倒置(priority inversion )
- 可能因为分配的时间片结束,持有互斥锁的线程被取消调度。这对于等待同一互斥锁的其他线程有不利影响,因为等待时间现在会更长。这个问题称为锁护送(lock convoying)
无锁编程的好处之一是一个线程被挂起,不会影响到另一个线程的执行,避免锁护送;在锁冲突频繁且平均锁定时间较短的系统,避免上下文切换和调度开销。
CAS (comapre and swap 或者 check and set),比较并替换,引用 wiki,它是一种用于线程数据同步的原子指令。CAS 核心算法涉及到三个参数,即内存值,更新值和期望值;CAS 指令会先检查一个内存位置是否包含预期的值;如果是这样,就把新的值复制到这个位置,返回 true;如果不是则返回 false。
CAS 对应一条汇编指令 CMPXCHG,因此是原子性的。
1 bool compare_and_swap (int *accum, int *dest, int newval)
2 {
3 if ( *accum == *dest ) {
4 *dest = newval;
5 return true;
6 }
7 return false;
8 }
一般,程序会在循环里使用 CAS 不断去完成一个事务性的操作,一般包含拷贝一个共享的变量到一个局部变量,然后再使用这个局部变量执行任务计算得到新的值,最后再使用 CAS 比较保存再局部变量的旧值和内存值来尝试提交你的修改,如果尝试失败,会重新读取一遍内存值,再重新计算,最后再使用 CAS 尝试提交修改,如此循环。比如:
1 void LockFreeQueue::push(Node* newHead)
2 {
3 for (;;)
4 {
5 // 拷贝共享变量(m_Head) 到一个局部变量
6 Node* oldHead = m_Head;
7
8 // 执行任务,可以不用关注其他线程
9 newHead->next = oldHead;
10
11 // 下一步尝试提交更改到共享变量
12 // 如果共享变量没有被其他线程修改过,仍为 oldHead,则 CAS 将 newHead 赋值给共享变量 m_Head 并返回
13 // 否则继续循环重试
14 if (_InterlockedCompareExchange(&m_Head, newHead, oldHead))
15 return;
16 }
17 }
上面的数据结构设置了一个共享的头节点 m_Head,当 push 一个新的节点时,会把新节点加在头节点后面;不要相信程序的执行是连续的,CPU 的执行是多线程并发。在 _InterlockedCompareExchange 即 CAS 之前,线程可能因为时间片用完被调度出去,新调度进来的线程执行完了 push 操作,多个线程共享了 m_Head 变量,此时 m_Head 已被修改了,如果原来线程继续执行,把 oldHead 覆盖到 m_Head,就会丢失其他线程 push 进来的节点。所以需要比较 m_Head 是不是还等于 oldHead,如果是,说明头节点不变,可以使用 newHead 覆盖 m_Head;如果不是,说明有其他线程 push 了新的节点,那么需要使用最新的 m_Head 更新 oldHead 的值重新走一下循环,_InterlockedCompareExchange 会自动把 m_Head 赋值给 oldHead。
五、ABA 问题
因为 CAS 需要在提交修改时检查期望值和内存值有没有发生变化,如果没有则进行更新,但是如果原来一个值从 A 变成 B 又变成 A,那么使用 CAS 检查的时候发现值没有发生变化,但实际上已经发生了一系列变化。
内存的回收利用会导致 CAS 出现严重的问题:
1 T* ptr1 = new T(8, 18);
2 T* old = ptr1;
3 delete ptr1;
4 T* ptr2 = new T(0, 1);
5
6 // 我们不能保证操作系统不会重新使用 ptr1 内存地址,一般的内存管理器都会这样子做
7 if (old1 == ptr2) {
8 // 这里表示,刚刚回收的 ptr1 指向的内存被用于后面申请的 ptr2了
9 }
ABA问题是无锁结构实现中常见的一种问题,可基本表述为:
- 进程P1读取了一个数值A
- P1被挂起(时间片耗尽、中断等),进程P2开始执行
- P2修改数值A为数值B,然后又修改回A
- P1被唤醒,比较后发现数值A没有变化,程序继续执行。
对于P1来说,数值A未发生过改变,但实际上A已经被变化过了,继续使用可能会出现问题。在CAS操作中,由于比较的多是指针,这个问题将会变得更加严重。试想如下情况:
有一个堆(先入后出)中有top指针和节点A,节点A目前位于堆顶,top指针指向A。现在有一个进程P1想要pop一个节点,因此按照如下无锁操作进行
1 pop()
2 {
3 do{
4 ptr = top; // ptr = top = NodeA
5 next_prt = top->next; // next_ptr = NodeX
6 } while(CAS(top, ptr, next_ptr) != true);
7 return ptr;
8 }
而进程P2在执行CAS操作之前打断了P1,并对堆进行了一系列的pop和push操作,使堆变为如下结构:
进程P2首先pop出NodeA,之后又Push了两个NodeB和C,由于内存管理机制中广泛使用的内存重用机制,导致NodeC的地址与之前的NodeA一致。
这时P1又开始继续运行,在执行CAS操作时,由于top依旧指向的是NodeA的地址(实际上已经变为NodeC),因此将top的值修改为了NodeX,这时堆结构如下:
经过CAS操作后,top指针错误的指向了NodeX而不是NodeB。
六、ABA 问题的解决
Tagged state reference,增加额外的 tag bits 位,它像一个版本号;比如,其中一种算法是在内存地址的低位记录指针的修改次数,在指针修改时,下一次 CAS 会返回失败,即使因为内存重用机制导致地址一样。有时我们称这种机制为 ABA‘,因为我们使第二个 A 稍微有点不同于第一个。tag 的位数长度会影响记录修改的次数,在现有的 CPU 下,使用 60 bit tag,在不重启程序10年才会产生溢出问题;在 X64 CPU,趋向于支持 128 bit 的 CAS 指令,这样更能保证避免出现 ABA 问题。
下面参考 liblfds 库代码说明下 Tagged state reference 的实现过程。
我们想要避免 ABA 问题的方法之一是使用更长的指针,这样便需要一个支持 dword 长度的 CAS 指令。liblfds 是怎么跨平台实现 128 bit 指令的呢?
在 liblfds 下,CAS 指令为 LFDS710_PAL_ATOMIC_DWCAS 宏,它的完整形式是:
LFDS710_PAL_ATOMIC_DWCAS( pointer_to_destination, pointer_to_compare, pointer_to_new_destination, cas_strength, result)
- pointer_to_destination: [in, out],指向目标的指针,是一个由两个 64 bit 整数组成的数组;
- pointer_to_compare: [in, out],用于和目标指针比较的指针,同样是一个由两个 64 bit 整数组成的数组;
- pointer_to_new_destination: [in],和目标指针交换的新指针;
- result: [out],如果 128 bit 的 pointer_to_compare 与 pointer_to_destination 相等,则使用 pointer_to_new_destination 覆盖 pointer_to_destination,result 返回 1;如果不相等,则 pointer_to_destination 不变,且 pointer_to_compare 的值变为 pointer_to_destination。
从上面可以看出,liblfds 库使用一个由两个元素组成的一维数组来表示 128 bit 指针。
Linux 提供了 cmpxchg16b 用于实现 128 bit 的 CAS 指令,而在 Windows,使用 _InterlockedCompareExchange128。只有 128 位指针完全相等的情况下,才视为相等。
参考 liblfds/liblfds7.1.0/liblfds710/inc/liblfds710/lfds710_porting_abstraction_layer_compiler.h 下面是关于 CAS 的 windows 实现:
1 #define LFDS710_PAL_ATOMIC_DWCAS( pointer_to_destination, pointer_to_compare, pointer_to_new_destination, cas_strength, result ) \
2 { \
3 LFDS710_PAL_BARRIER_COMPILER_FULL; \
4 (result) = (char unsigned) _InterlockedCompareExchange128( (__int64 volatile *) (pointer_to_destination), (__int64) (pointer_to_new_destination[1]), (__int64) (pointer_to_new_destination[0]), (__int64 *) (pointer_to_compare) ); \
5 LFDS710_PAL_BARRIER_COMPILER_FULL; \
6 }
再重点研究 new_top 的定义和提交修改过程。
new_top 是一个具有两个元素的一维数组,元素是 struct lfds710_stack_element 指针,两个元素分别使用 POINTER 0 和 COUNTER 1 标记。COUNTER 相当于前面说的 tag 标记,POINTER 保存的是真正的节点指针。在 X64 下,指针长度是 64 bit,所以这里使用的是 64 bit tag 记录 pointer 修改记录。
liblfds 用原 top 的 COUNTER + 1来初始化 new top COUNTER,即使用 COUNTER 标记 ss->top 的更换次数,这样每一次更换 top,top 里的 COUNTER 都会变。
只有在 ss->top 和 original_top 的 POINTER 和 COUNTER 完全相等的情况下,new_top 才会覆盖到 ss->top,否则会使用 ss->top 覆盖 original_top,下次循环用最新的 original_top 再次操作和比较。
参考 liblfds/liblfds7.1.0/liblfds710/src/lfds710_stack/lfds710_stack_push.c,无锁堆栈的实现:
1 void lfds710_stack_push( struct lfds710_stack_state *ss,
2 struct lfds710_stack_element *se )
3 {
4 char unsigned
5 result;
6
7 lfds710_pal_uint_t
8 backoff_iteration = LFDS710_BACKOFF_INITIAL_VALUE;
9
10 struct lfds710_stack_element LFDS710_PAL_ALIGN(LFDS710_PAL_ALIGN_DOUBLE_POINTER)
11 *new_top[PAC_SIZE],
12 *volatile original_top[PAC_SIZE];
13
14 LFDS710_PAL_ASSERT( ss != NULL );
15 LFDS710_PAL_ASSERT( se != NULL );
16
17 new_top[POINTER] = se;
18
19 original_top[COUNTER] = ss->top[COUNTER];
20 original_top[POINTER] = ss->top[POINTER];
21
22 do
23 {
24 se->next = original_top[POINTER];
25 LFDS710_MISC_BARRIER_STORE;
26
27 new_top[COUNTER] = original_top[COUNTER] + 1;
28 LFDS710_PAL_ATOMIC_DWCAS( ss->top, original_top, new_top, LFDS710_MISC_CAS_STRENGTH_WEAK, result );
29
30 if( result == 0 )
31 LFDS710_BACKOFF_EXPONENTIAL_BACKOFF( ss->push_backoff, backoff_iteration );
32 }
33 while( result == 0 );
34
35 LFDS710_BACKOFF_AUTOTUNE( ss->push_backoff, backoff_iteration );
36
37 return;
38 }
七、CAS 原理应用
- 无锁数据结构,参考 https://github.com/liblfds/li...
- 高性能内存队列disruptor中的CAS,参考 http://ifeve.com/disruptor/
- 数据库乐观锁
八、转载于
https://segmentfault.com/a/1190000018965585
本文来自博客园,作者:Mr-xxx,转载请注明原文链接:https://www.cnblogs.com/MrLiuZF/p/15143976.html