码迷,mamicode.com
首页 > 编程语言 > 详细

[原] 锁&锁与指令原子操作的关系 & 如何成就最快的多线程Queue?

时间:2015-11-07 10:47:17      阅读:506      评论:0      收藏:0      [点我收藏+]

标签:

  锁以及信号量对大部分人来说都是非常熟悉的,特别是常用的mutex。锁有很多种,互斥锁,自旋锁,读写锁,顺序锁,等等,这里就只介绍常见到的,

    互斥锁

      这个是最常用的,win32:CreateMutex-WaitForSingleObject-ReleaseMutex,linux的pthread_mutex_lock-pthread_mutex_unlock,c#的lock和Monitor,java的lock,这些都是互斥锁。互斥锁的作用大家都知道,是让一段代码同时只能有一个线程运行,

    自旋锁

      不常用,linux的pthread_spin系列函数就是自旋锁,(网上很多用原子操作写的自旋锁),作用和互斥锁大同小异。

    信号量

      win下的CreateSemaphore、OpenSemaphore、ReleaseSemaphore、WaitForSingleObject,linux也有同样的semaphore系列,还有c#的AutoResetEvent或者semaphore。这个用的也很多,信号两个状态,阻塞和通过,作用是保证多线程代码的业务顺序!

  先唠一唠这些锁的原理,(为什么我把信号量也归结于锁?)

    首先互斥锁,互斥锁实际上是由原子操作来实现的,

    比如,当变量A为0的时候为非锁,为1的时候为锁,当第一个线程将变量A从0变为1(原子操作)成功的时候,就相当于获取锁成功了,另外的线程再次获取锁的时候发现A为1了,(或者说两个线程同时获取锁->原子操作,某一个会失败),表示获取锁失败,当第一个线程用完了,就释放锁,将A=0(原子操作)。

    互斥锁的特点是,当锁获取失败了,当前代码上下文(线程)会休眠,并且把当前线程添加到这个内核维护的互斥锁的链表里,当后面的锁再次获取失败,也是将当前线程和执行信息放到这个链表里。当前占用的互斥锁的人用完了锁,内核会抽取互斥锁等待链表上的下一个线程开始唤醒继续执行,当内核链表上为空,就是没人抢锁了,就将锁状态设置为非锁,以次类推~

    然后呢,我们讲自旋锁,自旋锁很简单,他和互斥锁大同小异,区别就是不休眠,当获取锁失败了,就一直while(获取),一直到成功,所以,自旋锁在大部分场景都是不适用的,因为获取锁的时间里,cpu一直是100%的!!

    最后讲信号量,上面问为什么我将信号量也归结于锁这一类?

    因为信号量也是原子操作来实现的!道理和互斥锁一样的信号量也有一个链表,当等待信号的时候,系统也是把当前线程休眠,把线程和代码执行信息存储到这个信号量的链表里,当内核接受到信号的时候,就把这个信号量上的所有等待线程激活运行,这就是信号量!

    看了上面,是否明白互斥锁和自旋锁之间的使用场景呢?

原子操作

    到底什么原子操作?

    百度百科  所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

    所以,原子操作保证了多个线程对内存操作某个值得准确性!那么原子操作具体如何实现的?

    首先是inter cpu,熟悉汇编的人都知道,inter指令集有个lock,如果某个指令集前面加个lock,那么在多核状态下,某个核执行到这个前面加lock的指令的时候,inter会让总线锁住,当这个核把这个指令执行完了,再开启总线!这是一种最最底层的锁!!

    比如  lock cmpxchg dword ptr [rcx],edx  cmpxchg这个指令就被加锁了!

    inter指令参考可查阅http://www.intel.cn/content/www/cn/zh/processors/architectures-software-developer-manuals.html

    来自IA-32券3:

    HLT 指令(停止处理器)停止处理器直至接收到一个启用中断(比如 NMI 或 SMI,正 常情况下这些都是开启的)、调试异常、BINIT#信号、INIT#信号或 RESET#信号。处理 器产生一个特殊的总线周期以表明进入停止模式。 硬件对这个信号的响应有好几个方面。前面板上的指示灯会打亮,产生一个记录 诊断信息的 NMI 中断,调用复位初始化过程(注意 BINIT#引脚是在 Pentium Pro 处理器 引入的)。如果停机过程中有非唤醒事件(比如 A20M#中断)未处理,它们将在唤醒停 机事件处理之后的进行处理。

    在修改内存操作时,使用 LOCK 前缀去调用加锁的读-修改-写操作(原子的)。这种 机制用于多处理器系统中处理器之间进行可靠的通讯,具体描述如下: 在 Pentium 和早期的 IA-32 处理器中,LOCK 前缀会使处理器执行当前指令时产生 一个 LOCK#信号,这总是引起显式总线锁定出现。 在 Pentium 4、Intel Xeon 和 P6 系列处理器中,加锁操作是由高速缓存锁或总线 锁来处理。如果内存访问有高速缓存且只影响一个单独的高速缓存线,那么操作中就 会调用高速缓存锁,而系统总线和系统内存中的实际内存区域不会被锁定。同时,这 条总线上的其它 Pentium 4、Intel Xeon 或者 P6 系列处理器就回写所有的已修改数据 并使它们的高速缓存失效,以保证系统内存的一致性。如果内存访问没有高速缓存且/ 或它跨越了高速缓存线的边界,那么这个处理器就会产生 LOCK#信号,并在锁定操作期 间不会响应总线控制请求。

    IA-32 处理器提供有一个 LOCK#信号,会在某些关键内存操作期间被自动激活,去锁定系统总线。当这个输出信号发出的时候,来自其它处理器或总线代理的总线控制请求将被阻塞。软件能够通过预先在指令前添加 LOCK 前缀来指定需要 LOCK 语义的其它场合。在 Intel386、Intel486、Pentium 处理器中,明确地对指令加锁会导致 LOCK#信号的产生。由硬件设计人员来保证系统硬件中 LOCK#信号的可用性,以控制处理器间的内IA-32 架构软件开发人员指南 卷 3:系统编程指南170存访问。对于 Pentium 4、Intel Xeon 以及 P6 系列处理器,如果被访问的内存区域是在处理器内部进行高速缓存的,那么通常不发出 LOCK#信号;相反,加锁只应用于处理器的高速缓存(参见 7.1.4.LOCK 操作对处理器内部高速缓存的影响) 。

    可参考inter的 IA-32券3 第七章第一小节!

    当然inter还有其他方式保证原子操作!

    然后是ARM cpu, arm主要是靠两个指令来保证原子操作的,LDREX 和 STREX

    LDREX
      LDREX 可从内存加载数据。

      如果物理地址有共享 TLB 属性,则 LDREX 会将该物理地址标记为由当前处理器独占访问,并且会清除该处理器对其他任何物理地址的任何独占访问标记。

        否则,会标记:执行处理器已经标记了一个物理地址,但访问尚未完毕。

    STREX
      STREX 可在一定条件下向内存存储数据。 条件具体如下:

      如果物理地址没有共享 TLB 属性,且执行处理器有一个已标记但尚未访问完毕的物理地址,那么将会进行存储,清除该标记,并在Rd 中返回值 0。

      如果物理地址没有共享 TLB 属性,且执行处理器也没有已标记但尚未访问完毕的物理地址,那么将不会进行存储,而会在Rd 中返回值 1。

      如果物理地址有共享 TLB 属性,且已被标记为由执行处理器独占访问,那么将进行存储,清除该标记,并在Rd 中返回值 0。

      如果物理地址有共享 TLB 属性,但没有标记为由执行处理器独占访问,那么不会进行存储,且会在Rd 中返回值 1。

    参考:http://blog.csdn.net/duanlove/article/details/8212123

 

原子CAS操作

    首先看一段代码

int compare_and_swap (int* reg, int oldval, int newval)
{
    int old_reg_val = *reg;
    if (old_reg_val == oldval)
         *reg = newval;
    return old_reg_val;
}    

  cas即是Compare-and-swap,先比较再互换,即修改,意思就是,当reg等oldvalue的时候,将reg设置为newval,这段代码在非原子情况下(多线程)是没用的,但是如果这段代码是原子操作,那么他的威力就非常大, 互斥锁就和这个cas有关,

  上面我们也看到inter这个指令了,lock cmpxchgcmpxchg作用就是cas这个函数的作用比较并交换操作数,这就是cas原子操作,神奇吧,上面一个函数的作用,被inter一个指令搞定了,再cmpxchg前面加一个lock,那么这就是一个真正发挥威力的cas!

    在win32内核中有个InterlockedCompareExchange函数,这个函数就是cas功能,在inter cpu上的实现就是这段指令=》lock cmpxchg!!

    linux下有__sync_bool_compare_and_swap 和 __sync_val_compare_and_swap !

     在dotnet下有 interlocked.compareexchange!java参考sun.misc.Unsafe类!

CAS操作,到底有什么威力?

    如果要递加一个变量,在多线程下,应该要加锁,代码是这样的

int num = 0;
void add()
{
	lock();
	num = num + 1;
	unlock();
}

 

    但是如果不要锁,cas来操作??

int num = 0;
void add()
{
	int temp;
	do
	{
		temp = num;
	} 
	while (cas(num, temp, temp+1)==true)
}

  我们看到用一个do while来无限判断cas的修改结果,如果修改完成,那就成功+1,如果cas没有修改成功,继续while,temp将获取最新的num,再次cas操作!

  当一个线程的时候,num一个人操作,不会出现差错,当两个人的时候,某个人先进行cas原子操作,num+1,第二个线程拿着旧值去加操作,返现返回的就是false,于是重新复制temp获取最新的num,这就是cas的核心价值!无锁!

  cas其实这也算一种锁,乐观锁!相同于自旋锁也循环!

  以上两种操作是分在线程数上的,如果,开启的线程数<cpu线程数,上面的两种操作,cas必胜!,cas将会在最短的时间内完成多次add操作!!,但是如果线程过多,上面的代码就应该属于互斥锁胜利了,因为互斥锁有休眠!cas锁的缺点凸显很明显,就是不能集火!!会造成cpu100%。

   贴下互斥锁的代码(自己写的),

int i = 0;//0非锁,1锁住
//尝试获取锁,当cas返回失败,获取锁失败,返回true,获取锁成功 获取失败就休眠,等待系统唤醒
bool lock()
{
	return cas(i, 0, 1);
}
bool unlock()
{
	return cas(i, 1, 0);
}

 

CAS无锁Queue

    既然cas和自旋锁一个性质,为什么还用cas?我们在实际运用中,极致的集火访问某个变量的情况是很少的,一般都是访问后执行一段业务再访问,所以,cas在大部分的并发场景下是可用的,但是cas一般不用于普通业务下的线程编程锁,cas编程有很多陷阱!而且烧脑!!

    说到并发,多线程的队列,是被很多大神研究过,很多人都采用单线程或者互斥锁,都不是很理想!互斥锁有休眠和换新这个环节,耗人耗力!无锁,无休眠,cas才是能发挥多核cpu的极致张力!

    简单发下我写的cas环形队列,很简单的!

// .h

#pragma once

#ifndef _cas_queue
#define _cas_queue

#ifndef C_BOOL
#define C_BOOL

typedef int cbool;
#define false 0  
#define true  1

#endif

//
//typedef struct _cas_queue
//{
//	int size;
//} cas_queue;

#define QUEUE_SIZE 65536



#ifdef __cplusplus
extern "C" {
#endif
/*
compare and swap: CAS(*ptr,outvalue,newvalue);
return bool
*/

	cbool compare_and_swap(void ** ptr,long outvalue,long newvalue);

	void cas_queue_init(int queue_size);

	void cas_queue_free();

	cbool  cas_queue_try_enqueue(void * p);

	cbool cas_queue_try_dequeue(void ** p);


#ifdef __cplusplus
}
#endif

#endif


//.c
#include "cas_queue.h"

#ifdef _MSC_VER
#include <windows.h>
#else

#endif

volatile unsigned long read_index = 0;
volatile unsigned long write_index = 0;

long* read_index_p = &read_index;
long* write_index_p = &write_index;

void** ring_queue_buffer_head;

int ring_queue_size = QUEUE_SIZE;

cbool is_load = 0;

cbool compare_and_swap(void * ptr, long outvalue, long newvalue)
{
#ifdef _MSC_VER  // vs
	long return_outvalue = InterlockedCompareExchange(ptr, newvalue, outvalue);
	return return_outvalue == outvalue;
	/*InterlockedCompareExchange64 No success!!*/
	//#ifndef _WIN64 
	//	//32 bit
	//	long return_outvalue = InterlockedCompareExchange(ptr, newvalue, outvalue);
	//	return return_outvalue == outvalue;
	//#else
	//	//64 bit
	//	long return_outvalue = InterlockedCompareExchange64(ptr, newvalue, outvalue);
	//	return return_outvalue == outvalue;
	//#endif
#else
	//linux
#endif

}

void cas_queue_init(int queue_size)
{
	if (queue_size > 0)
		ring_queue_size = queue_size;
	int size = sizeof(void**)*ring_queue_size;
	ring_queue_buffer_head = malloc(size);
	memset(ring_queue_buffer_head, 0, size);
	is_load = 1;
	read_index = 0;
	write_index = 0;
}

void cas_queue_free()
{
	is_load = 0;
	free(ring_queue_buffer_head);
}

cbool cas_queue_try_enqueue(void * p)
{
	if (!is_load)
		return false;
	long index;
	do
	{
		//queue full
		if (read_index != write_index && read_index%ring_queue_size == write_index%ring_queue_size)
			return false;
		index = write_index;
	} while (compare_and_swap(&write_index, index, index + 1) != true);
	ring_queue_buffer_head[index%ring_queue_size] = p;

	return true;
}
cbool cas_queue_try_dequeue(void ** p)
{
	if (!is_load)
		return false;
	long index;
	do
	{
		//queue empty
		if (read_index == write_index)
			return false;
		index = read_index;
	} while (compare_and_swap(read_index_p, index, index + 1) != true);
	*p = ring_queue_buffer_head[index%ring_queue_size];
	return true;
}

    具体我测试过,在4个线程情况下,80万个消息,同时入和出,出完只需要150毫秒左右!!当然线程过多而且集火的话肯定会慢的!!

 

[原] 锁&锁与指令原子操作的关系 & 如何成就最快的多线程Queue?

标签:

原文地址:http://www.cnblogs.com/dark89757/p/4944304.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!