C/C++ -- 编程中的内存屏障(Memory Barriers) (2)
在前面的文章里,主要介绍了一下内存屏障的基本认识,和基本原理。本文针对之前的思路继续聊一聊该如何处理相应的问题,以及一些多线程程序编程的技巧。
- 1. Volatile关键字
- 2. Linux pthread线程锁
- 3. Linux gcc 4.2之后的__sync_fetch_and_add
- 4. 双Buffer实现Lock free无锁
- 4. 多读一写数据结构实现Lock free无锁
1. 在上篇文章中,提到了c/c++里的volatitle关键字,这个关键字的官方解释如下 :
"就象大家更熟悉的const一样,volatile是一个类型修饰符(type
specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果没有volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。"
--- 摘自《百度百科》
其核心旨意其实就是防止编译器对其进行优化(可以预防上文中提到的编译器导致的内存屏障),也就是每次cpu要获取一个变量,都要去内存中重新读取,而不会还缓存在自己的Cache中。应用的场景就例如while(!stop)这种大量执行判断的时候,每次都会去内存读取真实的值,而写入变量值的时候,也会写到内存地址中。但这样会组织编译器的优化,对这个变量的读取写入会比较慢(相对CPU的ns级别来说)。
2. 如果想彻底避免多线程之间的同步问题,或者临界区(多线程同时访问的集合)中不一定是一个变量,可能是一段代码一些逻辑,那么这时候,“锁”就会派上用场了:
“锁”的理念就是对临界区上一把“排他性”的标记,其他线程运行到这里时候,如果想获取同一把锁就要阻塞等待。Linux中的锁主要分为:pthread_mutex互斥锁/pthread_wrlock读写锁/pthread_spinlock自旋锁,几个的异同可以参考本人另外一篇文章《Linux锁的适用场景》。这样就可以对其间的代码访问进行同步,如:
- pthread_mutex_t lock;
- pthread_mutex_init(&lock,NULL);
- pthread_mutex_lock(&lock);
- g_count++;
- pthread_mutex_unlock(&lock);
Linux的“锁”,是一种强制性质“悲观锁”,也就是锁实现强制的,很容易导致“死锁”。并且性能不好,如果多个线程里访问临界区过多,锁过多的情况下,反而不如单线程的性能(此结论因项目环境而言)。所以单线程环境中必须去锁,在多线程环境下要尽量少锁,要尽量缩短lock->unlock中间的代码和时间。
3. 如果项目使用的编译器是gcc 4.2以上(可通过gcc -v查看),那么编译器内置了一种对atomic_t原子指令的支持:
- type __sync_fetch_and_add (type *ptr, type value);
- type __sync_fetch_and_sub (type *ptr, type value);
- type __sync_fetch_and_or (type *ptr, type value);
- type __sync_fetch_and_and (type *ptr, type value);
- type __sync_fetch_and_xor (type *ptr, type value);
- type __sync_fetch_and_nand (type *ptr, type value);
- type __sync_add_and_fetch (type *ptr, type value);
- type __sync_sub_and_fetch (type *ptr, type value);
- type __sync_or_and_fetch (type *ptr, type value);
- type __sync_and_and_fetch (type *ptr, type value);
- type __sync_xor_and_fetch (type *ptr, type value);
- type __sync_nand_and_fetch (type *ptr, type value);
- __sync_fetch_and_add (&globel,1);
- leal 40(%esp), %eax
- lock addl $1, (%eax)
- .loc 3 353 0
- movl 40(%esp), %edx
- movl $.LC7, %eax
写多线程的程序其实有的时候,可以通过代码技巧和算法来避免“锁”和“同步”,下面就来介绍一个lock free(无锁)的技巧。
4. 双Buffer切换实现Lock Free :
试想一个这样的场景,有两个线程,线程A负责处理用户的请求,相应用户请求,其间要从线程B的缓冲区获取一些数据,线程B的缓冲区是一块内存,这块内存定时或者不间断会更新一次,这块内存就成为了两个线程的临界区(线程B刷新此内存时,线程A可能正在读取)。
按照常规的做法,我们可能通过加锁来避免同时访问,但这样的话,因为这块内存可能没秒被访问100次,而30s更新一次,这样的话,每次访问都加锁和解锁,会带来很多无用功,性能也会下降。所以我们这里采用一种优化策略:双Buffer切换。顾名思义,就是有两个同样结构的内存块,同一时刻只有一个内存块提供服务,如果发生了更新或写入,则把新数据放在另一个内存块做,等到做完了,通过"指针"(此处的指针泛指指向资源的句柄)改变指向,指向新的内存块,即完成了切换,原内存块在新内存块完之前,仍然提供服务。
5. 对于一些多读一写的数据结构,这里用LinkLisk链表来举例:
多个线程读一个LinkList,后台有个线程会根据数据更新来写这个LinkList,也可以做到lock free。首先,拿插入来举例,插入的顺序决定了是否lock free。有两种情况:
一、先修改前面节点的next指针,再把新节点的next指针指向原来节点的下一个:
- node_t* tmp = pre_node->next;
- pre_node->next = new_node;
- new_node->next = tmp;
二,可以先修改新节点的next,然后修改原节点的next即可,不会出现访问异常。
类比这种方法,也可以推广到Hash表的冲突拉链中。所以,如果我们的上下文是多读一写的,就可以通过适当的编程技巧来避免过度的同步。