本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如有错漏,欢迎留言交流指正
什么是中断
?中断
硬件(电脑自身的硬件或者与电脑连接的外部设备)产生的一个电信号中断向量
传给cpu,cpu根据中断向量
(这里的中断id)在中断向量表
找到对应的中断函数
,来处理这个函数,处理分为上半部分
和下半部分
。
异常
:CPU内部出现的中断
IF
(中断标志,rflag或者eflag中断标志寄存器中的一个位,用来做中断的开启或者屏蔽)维持不变
(当cpu产生异常
的时候,中断不会
被屏蔽的,还可以响应
其他中断请求)故障
(FALT) 如除0,缺页,越界,堆栈段错误(客观的)陷阱
(TRAP)如调试指令int 3
了,溢出
等,故意为之(人为的有目的的)中断
(外部中断): IF标志清零(关中断,在处理完当前中断之前,不响应
其他中断,直到IF标志置1
)中断向量
:0-255(1Byte)
IO
引起的屏蔽中断(由中断控制器连接的硬件)0x80
系统调用system_call()
进入内核无中断
:
PASSIVE_LEVEL
(0) DriverEntry()、DriverUnload(),分发函数处于这个级别软中断
:
APC_LEVEL
(1)DISPATCH_LEVEL
(2) 完成函数(在irp完成的时候调用的函数)、NDIS(防火墙)回调函数
处于这个级别 (做内核开发不涉及硬件的话,最高的中断级别就不会超过
在这个级别的)硬中断
:
DIRQL
设备中断请求级处理程床执行PROFILE_LEVEL
配置文件定时器CLOCK2_LEVEL
时钟SYNCH LEVEL
同步级IPI LEVE
处理器之间中断级POWER LEVEL
电源故障级分页内存
)
PASSIVE_LEVEL
级别可以使用任何
函数和内存DISPATCH_LEVEL
级别只能访问能运行
在DISPATCH_LEVEL
级别的API和非分页内存
IRQL
比如:需要在高
IRQL中处理低
IRQL的一些事情。键盘木马,键盘过滤驱动记录按键消息,把数据拿到再存到文件中,上传到服务器上。但按键消息只能在完成函数(DISPATCH_LEVEL,不能
进行文件的读写)拿到,这时候需要开启一个工作者线程
,把IRQL降低,在工作者线程把数据存为文件即可。NONPAGEPOOL
内存可在任何
级别使用PAGEDPOOL
只能在PASSIVE_LEVEL
和APC_LEVEL
不能保证
代码遵循了IRQL级别,比如知道当前的代码是PASSIVE_LEVE
,但不能保证代码里面是否有IRQL不匹配
,就可使用在PASSIVE_LEVEL
和APC_LEVEL
级别代码中加:PAGED_CODE()
宏。/// 这个宏就是用来判断,如果当前代码的IRQL > APC_IRQL,就会ASSERT(FALSE);抛出异常,系统蓝屏
/// 只在调试版本有效
#define PAGED_CODE()\
{if (KeGetCurrentIrql() > APC_LEVEL){\
KdPrint(("EX: Pageable code called at IRQL %d\n",KeGetCurrentlrql()));\
ASSERT(FALSE);}\
}\
变量
和数据
,包括所有的寄存器变量
、进程打开的文件内存信息
等。执行中断函数
的时候,就称系统处在中断上下文
里面,这时候系统代替硬件去做一些事情,处在硬件的环境中,由于硬件环境没有
进程调度的机制,所以系统如果处于中断上下文的话是不可睡眠的,因为一但睡眠,就无法把你切换回来了,也再调度其他进程执行,系统就死掉了
。
中断上下文
:其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)进程上下文
,处于进程上下文是可以睡眠的,因为睡眠之后,还可以通过调度机制重新把你切换回来
。进程的上下文
可以分为三个部分:
用户级上下文
:正文数据、用户堆栈以及共享存储区:寄存器上下文
:通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)栈指针(EFLAGS)系统级上下文
:进程控制块task struct、内存管理信息mm struct、vm_area_struct、pgd、pte)、内核栈进程上下文
或者中断下文
。
系统调用服务
的内核代码,代表发起系统调用
的应用程序运行在进程上下文
,代表进程运行。异步
运行在中断上下文
,代表硬件运行,中断上下文和特定进程无关
。运行在中断上下文的代码
就要受一些限制,不能
做下面的事情:
睡眠或者放弃CPU
(硬件与人睡眠例子)wake()
,异步读循环等待
,内存分配函数
。尝试获得信号量
执行耗时的任务
大量服务
和请求中断上下文
占用CPU时间太长会严重影响系统功能虚拟地址
什么是线程?
多线程
的)目的
:提高系统并发的度(某一个时间段多程序执行,但具体到某一个时刻只有一个程序)最小单位
,亦即执行处理机调度的基本单位
。如果把进程
理解为在逻辑上操作系统所完成的任务
,那么线程
表示完成该任务的许多可能的子任务
之一。独立调度执行
。这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性
,线程带来的好处包括:
易于调度
。线程是系统调度的基本单位,线程的切换比进程要快。提高并发性
。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一
程序的不同
部分。(左右手画画,唱歌)开销少
,创建线比创建进程要快,所需开销很少,仅占有量的资源,比如栈和寄存器。多处理器
的功能。通过创建多线程进程(即进程可具有两个或更多个线程)每个线程在各个处理器上运行,从而实现应用程序的并发性使每个处理器都得到充分
运行。超线程
2002年
发布。超线程的英文是HT
技术,全名为Hyper-Threading,中文又名超线程
。同一时间里
,应用程序可以使用芯片的不同部分
。
单线程芯片
每秒钟能够处理成千上万条指令,但是在任一时刻只能够对一条指令
进行操作。而超线程技术可以使芯片同时
进行多线程处理,使芯片性能得到提升。同时
执行两个
线程,但它并不像两个真正
的CPU那样,每个CPU都具有独立的资源
(寄存器)。当两个线程都同时需要某一个资源时,其中一个要暂时停止
,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能(连体人例子)Die
:CPU核心,又称为内核,就是CPU上面中间的小方块
,里面是CPU的核心,是CPU最重要的组成部分。CPU中心那块隆起的芯片就是核心,是由单晶硅以一定的生产工艺制造出来的,CPU所有的计算
、接受
/存储
命令、处理数据
都由核心执行。各种CPU核心都具有固定的逻辑结构,一级缓存、二级缓存、执行单元、指令级单元和总线接口
等逻辑单元都会有科学的布局。为了便于CPU设计生产、销售的管理CPU制造商会对各种CPU核心给出相应的代号
,这也就是所谓的CPU核心类型
。核心面积(这是决定CPU成本的关键因素,成本
与核心面积
基本上成正比
)Logical CPU Pointer(逻辑处理单元)
。因此新一代的P4 HT的die的面积比以往的P4增大了5%。而其余部分如ALU(算数逻辑运算单元)、FPU(浮点运算单元)L2Cache (二级缓存)
则保持不变,这些部分是被分享的。与进程关系
:
只能
属于一个进程,而一个进程可以有多个线程,但至少
有一个线程。进程通信
的办法实现同步
。程序
至少有一个进程
,一个进程至少有一个线程
。与进程区别
:
资源分配单位
。线程除了拥有部分寄存器
和栈
外(切换线程的时候,就需要对线程上下文(栈和寄存器)进行备份),不拥有系统资源
,但可以访问属于
进程的资源。调度单位
,即线程拥有CPU,即在CPU运行的是线程。运行一定时间片就会被切换。系统开销
:在创建或撤消进程
时由于系统都要为之分配和收资源,导致系统的开销明显大于创建或撤消线程时的开销。同步
:线程与线程之间合作
互斥
:线程之间竞争
带来什么问题?
(R3的例子说明)
分发函数
为什么处在多线程环境下
?
设置排他性
,即该设备对象可以被多个进程
打开,每个Irp请求都会发给分发函数处理,分发函数处在多进程环境
。肯定是
多线程。即使在同一个
进程,也可以开启多线程,在每个线程都向设备对象发送Irp请求,对应的分发函数可以处理多个线程
的Irp请求,自然也是多线程环境多线程编程
都包含三个组成部分:
执行函数
;(线程代码)创建函数
;(如何创建线程)数据同步机制
(如何保证多线程数据安全)执行的代码
,需要通过编程去实现,把要在新的线程中完成的任务
放在该执行函数中
去完成;创建
,它需要线程执行函数
作为参数
,然后线程执行函数自己的参数
也做为参数之一(如果线程执行函数自己存在
参数的话)数据同步机制
则负责多线程执行环境条件下数据的完整性
和一致性
。CreateThread
:是Windows的API函数(SDK函数的标准形式,直截了当的创建方式,任何场合
都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程
。不调用
MFC和CRT的函数时,可以用Create Thread,其它情况不要使用。因为:
纪录
和初始化
,以保证C函数库
工作正常。知道
新线程的创建,也需要做一些初始化工作
(分配内存)。malloc()
,fopen()
,_open()
,strtok()
,ctime()
,或localtime()
等函数需要专门的线程局部存储
的数据块
,这个数据块通常需要在创建线程
的时候就建立
,如果使用CreateThread,这个数据块就没有建立
,但函数会自己建立
一个,然后将其与线程联系
在一起,这意味着如果你用CreateThread来创建线程,然后使用这样的函数,会有块内存在不知不觉
中创建,而且这些函数并不
将其删除,而CreateThread
和ExitThread
也无法知道
这件事,于是就会有Memory Leak
,在线程频繁启动的软件中,迟早会让系统的内存资源耗尽
。_beginthreadex
:MS对C Runtime库的扩展SDK函数,首先针对C Runtime库做了一些初始化
的工作,以保证C Runtime库工作正常。然后,调用CreateThread
真正创建线程AfxBeginThread
:MFC中线程创建的MFC函数,首先创建了相应的CWinThread对象,然后调CWinThread::CreateThread,在CWinThread::CreateThread中完成了对线程对象的初始化
工作,然后调用_beginthreadex(AfxBeginThread相比较更为安全
)创建线程。它让线程能够响应消息
,可用于界面
线程,也可以用于工作者
线程。pthread_create
: LINUX平台AfxBeginThread
:在MFC中用,工作者线程/界面线程_beginthreadex
:调用C运行库
的,应该用这个,但不能用在MFC
中。CreateThread
:工作者线程,MFC中不能
用C runtime 中不能用。所以r任何
时候最好都不要用
。AfxBeginThread
>_beginthreadex
>CreateThread
/// 线程执行函数
void DoFind(IN PVOID pContext)
{
/// 为了让子线程常驻内存,写死循环,某些情况(比如用全局变量作为标志)才break退出,
while(1)
{
if(something) break;
}
}
/// 主线程,需要等待子线程的结束,否则,如果主线程比子线程提前结束,子线程就成了`孤魂野鬼`
void StartThread()
{
HANDLE hThread = NULL;
PVOID objtowait = 0;
/**
* @brief PsCreateSystemThread 线程创建函数,创建线程,创建成功之后,会执行DoFind函数,直到DoFind函数返回,PsCreateSystemThread才返回;
* @param[out] hThread 创建线程的句柄
* @param[in] DoFind 函数地址,指向线程执行的代码;
* @param[in] NULL(倒数第一个)结构体,传给线程执行函数的某些参数封装到结构体中;比如传ip地址和端口
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-27 22:09
*/
NTSTATUS dwStatus =
PsCreateSystemThread(
&hThread,0,NULL,(HANDLE)0,
NULL,DoFind,NULL
);
///
if (!NT_SUCCESS(dwStatus)
{
return;
}
/**
* @brief ObReferenceObjectByHandle 获取新创建子线程的句柄;
* @param[in] hThread PsCreateSystemThread()所创建线程的句柄;
* @param[out] &objtowait 由线程句柄得到线程的内核对象
* @param[in] NULL(第一个)不应该传NULL,要指定线程的类型;
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-27 22:09
*/
ObReferenceObjectByHandle(
hThread,
THREAD_ALL_ACCESs,
NULL,
KernelMode,
&objtowait,
NULL
)
/// 等待子线程的内核对象退出,只要该线程没有退出,这个函数会一直在这里等待,如果该线程退出来了,objtowait的状态就会发生改变,函数就会返回,继续往下执行。
/// 如果主线程提前结束,主线程传给子线程的参数是在主线程定义局部变量,已经销毁了,就成了野指针,子线程再访问这些变量的时候,就可能出问题了。
KeWaitForSingleobject(objtowait,Executive,KernelMode,FALSE,NOE);
ObDereferenee(&objtowait);
return;
}
Event两个状态
:
Event两个类别
:
Notification
事件:不自动恢复,需要手动设置,比如卧室的灯打开了,如果不手动关灯,就会一直亮着synchronization
事件:自动恢复,比如声控灯KeWaitForSingleObject(一次等一个对象)/KeWaitForMultipleObject(一次等多个对象)
等待事件的发生LARGE_INTEGER TimeOut = {0};
/// 等待时间必须是负数,等待10s
TimeOut.QuadPart = -10* 10000000i64;
KeWaitForSingleObject(
&kSemaphore, //如果等待进程的事件发生了,就会把信号量+1,KeWaitForSingleObject()检测到kSemaphore变化了,函数返回,就会退出等待。
Executive,
KernelMode,
RFALSE,
&TimeOut //TimeQut==0.不等待;NULL,无限等待
);
/// 线程A:
/// 定义开灯事件
KEVENT waitEvent;
/// 初始化事件,NotificationEven表示设置为不自动恢复事件,FALSE表示最初为无信号状态
/// 这里没有给事件起名字,可以给事件起名字
KeInitializeEvent(&waitEvent,
NotificationEvent,
FALSE);
/// 开灯 会将线程B从睡眠中唤醒
KeSetEvent(&waitEvent,
IO_NO_INCREMENT,
FALSE);
/// 线程B:
/// 等灯 在等待线程A的开灯事件的过程中会睡眠,让出CPU,直到线程A开灯,线程B才会被唤醒,KeWaitForSingleObject函数返回,继续往下执行
KeWaitForSingleObject(
&waitEvent,
Executive,
KernelMode,
FALSE,
NULL); ///< TimeQut=0,不等待;NULL,无限等待
/// 关灯 把事件状态设置成无信号状态
KeClearEvent(&waitEvent);
KeResetEvent(&waitEvent);
/// 方式1:轮询
/// 缺点:效率问题
while(1)
{
///发送DeviceloControl下去拿数据
Sleep(1000);
}
/// BaseNamedObjects固定的,ProcEvent可以自己定义
L"\\BaseNamedObjects\\ProcEvent"
IoCreateNotificationEvent
/// 在创建设备对象的时候,可以分配一段空间作为设备扩展,设备扩展可以存放我们任何想存放的数据
/// 利用设备扩展存放创建的监视的进程信息,一旦通知的应用层,应用层就会下来从设备扩展里面把数据拿到
typedef struct _DEVICE_EXTENSION
{
HANDLE hProcessHandle; ///< 事件对象句柄
PKEVENT ProcessEvent; ///< 用户和内核通信的事件对象指针
HANDLE hParentId; ///< 创建进程的父id
HANDLE hProcessId; ///< 创建进程的id
BOOLEAN bCreate; ///< 表示进程是创建还是销毁
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
/// 指定设备扩展的大小,这样创建出来的设备对象就有一个设备扩展
IoCreateDevice( DriverObject, sizeof(DEXICE_EXTENSION); &ustrDeviceName ...)
/// 访问:
//获得DEVICE_EXTENSION结构
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;
/// 保存信息
deviceExtension->hParentId = hParentId;
deviceExtension->hProcessId = PId;
deviceExtension->bcreate =bCreate;
/// 触发事件,通知应用程序
/// 设置事件为有信号
KeSetEvent(deviceExtension->ProcessEvent, 0,FALSE);
/// 用完之后,把事件恢复到无信号
KeClearEvent(deviceExtension->ProcessEvent);
/// Global在XP是不用加的,在win7以上系统事件前面一定要加Global
/// XP下服务程序和应用程序创建的内核对象的命名空间默认是全局的,而Win7后不是,服务创建的内核对象默认在session0下,
/// 用户创建的内核对象默认在各自的session下(一次登陆就是一个session),解决此问题的方法为在创建命名时间对象时指定名字是全局的,即带上Global
#define EVENT_NAME L"Global\\ProcEvent"
/// 打开内核事件对象
HANDLE hProcessEvent = ::OpenEventW(SYNCHRONIZE, FALSE,
EVENT_NAME);
/// 等待事件,INFINTE等待的时间是无限的,有事件触发的时候函数返回,然后进入while循环
while (::WaitForSingleObject(hProcessEvent, INFINTE))
{
/// bugs to fix here
/// 发送DeviceloControl下去拿数据
}
正确
,编译出来的监控程序,当有其他程序打开的时候,监控程序就退出错误
,编译出来的监控程序,能监控进程的创建和销毁,但CPU占用会达到100%,好像变成了轮询
同一个
设备扩展中,如果系统创建的进程比较多,应用层来不及拿数据,新数据会把老数据覆盖掉
链表
,以保证能把数据长期保存KSPIN_LOCK
自旋锁ERESOURCE
读写锁共享锁FAST_MUTEX
快速互斥锁KSPIN_LOCK
自旋锁不要超过25微秒
)的情况。而Mutex
不是,它会系统阻塞请求线程。如果你需要长时间串行化访问
一个对象,应该首先考虑使用互斥量而不是自旋锁。DISPATCH_LEVEL
,Mutex不会。DISPATCH_LEVEL
及以下级别都可以请求Spinlock。Mutex通常在PASSIVE_LEVEL
请求,在DISPATCH_LEVEL
上TIMEOUT需设为0。非递归锁
”,即不能递归获得该锁,而Mutex是“递归锁”。ERESOURCE
较好DISPATCH_LEVEL
PASSIVE_LEVEL
调用的函数/// 定义
KIRQL OldIrql; ///< 获取自选锁的时候,IRQL会上升,需要提前保存原来的IRQL用来恢复原始状态
KSPINLOCK mySpinLockProc; ///< 存放要拿的自旋锁
/// 获得自旋锁并保存原来的状态,拿的过程是轮询是忙等,拿不到会在这一直拿,不会让出CPU,不会阻塞睡眠
KeAcquireSpinLock(
&mySpinLockProc,
&OldIrql);
/// 对数据进行操作和访问
g_i++; ///< 全局资源,尽量快完成操作
...
/// 释放自旋锁并恢复原来的状态
KeReleaseSpinlLock(
&mySpinLockProc,Oldlrqll.
ERESOURCE
读写锁共享锁/// 把ERESOURCE封装在结构体里面,封装一下好看一点,看名字就知道是锁
/// 把拿锁释放锁删除锁操作封装成单独的函数,用起来方便些
typedef struct _MY_LOCK
{
ERESOURCE m_Lock;//用于互斥
}MY_LOCK;
/// 初始化
/// __stdcall表示 1.参数从右向左压入堆栈 2.函数被调用者修改堆栈
VOID __stdcall InitLock(MY_LOCK* IpLock)
{
ExInitalizeResourceLite(&lpLock->m_Lock);
}
/// 拿写锁
VOID __stdcall LockWrite(MY_LOCK* IpLock)
{
/// 进入临界区
KeEnterCriticalRegion();
/// Exclusive
ExAcquireResourceExclusiveLite(&lpLock->m_Lock,TRUE)
}
/// 释放锁
VoID stdcall UnLockWrite(MY_LOCK* lpLock)
{
ExReleaseResourceLite(&lpLock->m_Lock);
/// 退出临界区
KeLeaveCriticalRegion();
}
/// 拿读锁与拿写锁类似前面
VoID_stdcall LockRead(MY_LOCK* lpLock)
{
/// 进入临界区
KeEnterCriticalRegion0;
/// Shared
ExAcquireResourceSharedLite(&lplLock->m_Lock,TRUE);
}
/// 释放锁
VoID stdcall UnLockWrite(MY_LOCK* lpLock)
{
ExReleaseResourceLite(&lpLock->m_Lock);
/// 退出临界区
KeLeaveCriticalRegion();
}
/// 以让等待读优先的方式去拿读锁
VOID __stdcall LockReadStarveWriter(MY_LOCK* lpLock)
{
KeEnterCriticalRegion();
/// 让等待中的读优先,StarveExclusive使饥饿,使写进程饥饿
/// 有多个线程等待同一资源,当有锁释放的时候,先满足读的线程还是先满足写的线程
ExAcquireSharedStarveExclusive(&lpLock->m_Lock, TRUE);
/// 让等待中的写优先
/// ExAcquireSharedWaitForExclusive(&lpLock->m_Lock, TRUE);
}
/// 用完之后在DriverUnderUnload把锁删掉
VOID __stdcall DeleteLock(MY_LOCK* IpLock)
{
/// 不在卸载的时候删除锁,会蓝屏
ExDeleteResourceLite(&lpLock->m_Lock);
}
/// 全局资源,链表
LIST_ENTRY g_WaitList;
/// 代码在多线程环境下,所以需要定义一个锁来保护这个链表
MY_LOCK g_WaitListLock;
/// DriverEntry里通过这个函数初始化锁
InitLock(&g_WaitListLock);
/// 多线程环境里
/// 拿写锁
LockWrite(&g_WaitListLock);
/// 从链表中找到节点
IpWaitEntry = LookupWaitEntryByID(&g_WaitList, IpReply->m_ulWaitID);
/// 修改找到的节点的内容
if (IpWaitEntry != NULL)
{
IpWaitEntry->m_bBlocked = IpReply->m_ulBlocked;
KeSetEvent(&lpWaitEntry->m_ulWaitEvent, O, FALSE);
///return; ///< 这里不能return,因为返回在锁释放之前,之前申请的锁没有释放,会导致死锁(其他进程等到锁)
}
/// 释放锁
UnLockWrite(&g_WaitListLock);
/// 在DriverUnload中调用这个函数删除锁
DeleteLock(&g_WaitListLock);
FAST_MUTEX
快速互斥锁MUTEX
与 KSPIN_LOCK
自旋锁和ERESOURCE
读写锁共享锁相比性能是最差的,所以才出现了FAST_MUTEX
,而且在windows内核中基本上都不用MUTEX
了APC_LEVEL
KSPIN_LOCK
自旋锁一样,是“非递归锁
”,即不能递归获得该锁,而Mutex
是“递归锁”。/// 定义一个FAST_MUTEX锁
FAST_MUTEX gSfilterAttachLock;
/// 初始化
ExInitializeFastMutex( &gSfilterAttachLock);
/// 拿锁
ExAcquireFastMutex( &gSfilterAttachLock );
/// ExAcquireFastMutex( &gSfilterAttachLock ); ///< 这里不能两次获取锁,因为和FAST_MUTEX和`KSPIN_LOCK`自旋锁一样,是“`非递归锁`”
//Do something here
...
/// ExReleaseFastMutex( &gSfilterAttachLock ); ///< 这里不能两次释放锁,因为和FAST_MUTEX和`KSPIN_LOCK`自旋锁一样,是“`非递归锁`”
ExReleaseFastMutex( &gSfilterAttachLock);
KSEMAPHORE
信号量kill -9
杀掉进程KEVENT
,但多数情况下的用法是表示一个资源能被多个线程共享,比如你的女盆友只能允许你一个人访问,而老师就可以被多个同学访问/// 定义一个信号量
KSEMAPHORE kSemaphore;
/// 初始化信号量
KeInitializeSemaphore(
&kSemaphore,
1, ///< 信号量的初始值,资源数
2 ///< 信号量的最大值,可以在初始化的时候把设置为1,1,这样就把信号量退化成了互斥体了,在早期的linux就是通过信号量去模拟互斥体的
);
LARGE_INTEGER waitTime = {0};
waitTime.QuadPart = -1*10000000i64;
/// 消耗资源,对信号量进行减一
KeWaitForSingleObject(&kSemaphore,
Executive,
KernelMode,
FALSE,
&waitTime ///< O,立即返回;
///< NULL,无限等待(如果信号量为0(即没有资源了),函数就会一直等,等有资源了才会返回。);
///< 负数 等待|负数|秒
);
/// 释放资源,对信号量进行加一
KeReleaseSemaphore(&kSemaphore,
1,
IO_NO_INCREMENT,
1,
FALSE);
SpinLock
ERESOURCE
/FAST_MUTEX
KEVENT
/KSEMAPHORE
KEVENT
a<-->b
交换以Interlocked
开头是原子操作,原子操作是不会被打断的)单核多线程
,也一定要加锁,是因为g_count++
,在C语言中是一条指令,但一条指令不代表是原子操作,在编译的时候,实际的被翻译成了多条汇编指令,如果线程在执行这些汇编指令的时候,还没全部执行完就被切换出去了,这样就会出现问题。/// 对全局资源增减指令,假设g_count为0
g_count++;
/// 对应的汇编指令
/// 把全局资源放在eax寄存器中,准备下一步用eax累加,如果线程A执行到这里的时候时间片用完了,切换之前备份进程上下文(把eax寄存器备份起来,全局资源g_count这时还没发生变化,还是0)
/// A线程切换出去之后,B线程进来了,也把全局资源放在eax寄存器中,执行完整的3条汇编指令之后,全局资源g_count这时发生了变化,变成1,然后B线程被切换出去了
/// A线程被切换回来,恢复A线程的上下文,eax恢复为0,继续执行add eax,1,mov dword ptr[i],eax之后全局资源g_count还是1(正确值应该是2,因为之前B线程已经+1了)
mov eax dword ptr[i]
add eax,1
mov dword ptr[i],eax
Critical Section
与Mutex
作用非常相似,但Mutex是可以命名的,也就是说它可以跨越进程
使用。所以创建互斥量需要的资源更多,如果只为了在进程内部使用的话(同一进程不同线程中互斥
),使用临界区会带来速度上的优势并能够减少资源占用量(不断进行锁的升级)。Mutex
),信号量(Semaphore
), 事件(Event
)都可以跨越进程
来进行同步数据操作(一个进程创建之后,另外的进程可以通过名字打开它,从而用于进程间的数据同步)Mutex
可以指定资源被独占的方式使用,但如果一个资源允许N(N>1)个进程或者线程访问,这时候如果利用Mutex就没有办法完成这个要求,Semaphore
可以,当作一种资源计数器。FAST_MUTEX
不能命名)Critical_Section使用
Critical_Section的实现
/// 不能跨进程,只能用于同一进程不同线程中互斥
struct RTL_CRITICAL_SECTION
{
/// Debuglnfo 此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为RTL_CRITICAL_SECTION_DEBUG
PRTL_CRITICAL_SECTION_DEBUG Debuglnfo;
/// LockCount这是临界区中最重要的一个字段。它被初始化为数值-1;此数值等于或大于0时,表示此临界区被占用。当其不等于-1时,OwningThread字段包含了拥有此临界区的线程ID。此字段与(RecursionCount-1)数值之间的差值表示有多少个其他线程在等待获得该临界区。
LONG LockCount;
/// RecursionCount此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。
LONG RecursionCount;
/// OwningThread此字段包含当前占用此临界区的线程的线程标识符。此线程ID与GetCurrentThreadld之类的API所返回的ID相同。
HANDLE OwningThread;
/// LockSemaphore它实际上是一个自复位事件(event),而不是个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用DeleteCriticalSection (它将发出一个调用该事件的 CloseHandle调用,并在必要时释放该调试结构),否则将会发生资源泄漏。
HANDLE LockSemaphore;
/// SpinCount仅用于多处理器系统。在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转dwSpinCount次(轮询)。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作(等待的过程是会进入内核的,会睡眠,操作系统会把线程放入等待队列里面去,直到锁释放之后,再通过Semaphore将其唤醒,是很耗时间的)。旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用InitializeCriticalSectionAndSpinCount API将其设置为一个不同值。
ULONG_PTR SpinCount;
};
实现CAutoLocker
(自动锁,类似智能指针)利用C++的面向对象机制
/// 用C++的类封装Critical_Section
/// 还不是自动的,还需要手动调用Lock()和UnLock()
class CLock
{
public:
/// 加锁
void Lock()
{EnterCriticalSection(&m_sec);}
/// 释放锁
void Unlock()
{LeaveCriticalSection(&m_sec);}
/// 在构造函数里面初始化锁
CLock()
{InitializeCriticalSection(&m_sec);}
/// 再析构函数里面删除锁
~CLock()
{DeleteCriticalSection(&m_sec);}
private:
CRITICAL_SECTION m_sec;
};
/// 在封装的CLock基础上继续封装一层
class CAutoLock{
public:
/// 在构造函数里面加锁
CAutoLock(CLock * pLock):m_pLock(pLock) ///< 等价于m_pLock=pLock
{
m_pLock->Lock);
}
/// 再析构函数里面释放锁
~CAutoLock)
{m_pLock->Unlock();}
private:
CLock* m_pLock;
};
///
/// 定义一个全局锁
CLock g_lock;
///使用例子:在函数内部使用CAuroLock
{
/// 生成一个匿名对象,构造对象的时候,初始化锁,加锁
CAutoLock a(&g_lock);
...
}
/// 函数退出后,自动销毁匿名对象,析构函数释放锁,删除锁
/// 与手动加锁相比
/// 优点:自动化
/// 缺点:粒度不好把握,如果{...}内部代码很长,包含循环的情况,锁就迟迟得不到释放,其他线程会等很久,性能下降。
/// 改进方法:在函数内部继续加‘{}’来精准限定锁的范围
/// 基于链表的栈
typedef struct _node
{
int value;
struct _node *next;
}node,*pnode;
/// 全局变量栈顶指针没有加锁
node *top = NULL;
/// 改进:定义一个锁来保护全局变量
ICRITICAL_SECTION g_cs;
/// 在创建栈之前,初始化锁
InitializeCriticalSection(&g_cs);
CreateStack();
/// 在销毁栈之后,删除锁
DeleteCriticalSection(&g_cs);
/// 创建两个线程,把出栈和入栈(访问全局变量的操作)用线程去实现
unsigned tid = 0;
HANDLE hArray [2] = {0};
hArray[0] = (HANDLE)_beginthreadex(NULL,
0,
PushProc,
NULL,
0,
&tid);
hArray[1] = (HANDLE)_beginthreadex (NULL,
0,
Pop,
NULL,
0,
&tid);
UINT WINAPI PushProc (LPVOID arg)
{
/// 如果当前有其他线程在堆栈进行操作时,则等待该线程操作完栈(栈顶指针释放),当前进程才可以对栈进行操作
EnterCriticalSection(&g_cs);
for (int i=0;i<100;i++)
{
Push(i);
}
LeaveCriticalSection(&g_cs);
return 1;
}
UINT WINAPI PopProc(LPVOID arg)
{
int v = 0 ;
/// 同上
EnterCriticalSection(&g_cs);
while(!IsStackEmpty())
{
Pop (&v);
printf ("%d ", v);
}
LeaveCriticalSection(&g_cs);
printf("\n") ;
return 1 ;
}
/// 最终的测试效果是,pop的时候打印输出结果是99-0,证明push也是完整的从0-99,所以加了锁之后的push和pop对全局资源栈顶指针的访问时是串行的
/// 创建一个Mutex,Mutex是一个HANFDLE类型
HANDLE hMutex = NULL;
hMutex = CreateMutex(NULL, FALSE, NULL);
/// 加锁
WaitForSingleObjeat(hMutex,INFINITE);
//WaitForMultipleObjects(2, hArray,TRUE,INFINITE);
/// 放锁
ReleaseMutex(hMutex);
///关闭
CloseHandle(hMutex);
/// 这里用到了WIN32的函数,需要把这些头文件包含进来
#include
#include
#include
/// 要保护的全局变量
int g_iTotal = 0;
/// 创建一个Mutex,Mutex是一个HANFDLE类型
HANDE g_hMutex = NULL;
/// 创建一个Mutex来保护g_iTotal
g_hMutex = CreateMutex(NULL,FALSE,NULL);
/// 创建两个线程,func1,func2
unsigned tid = 0;
HANDLE hArray [2] = {0};
hArray[0] = (HANDLE)_beginthreadex(NULL,
0,
func1,
NULL,
0,
&tid);
hArray[1] = (HANDLE)_beginthreadex (NULL,
0,
func2,
NULL,
0,
&tid);
UINT WINAPI func1(LPVOID arg)
{
/// 如果当前有其他线程在堆栈进行操作时,则等待该线程操作完栈
for (int i=0;i<5;i++)
{
WaitForSingleObjeat(hMutex,INFINITE);
g_iTotal++;
printf("t1:%d\n",g_iTotal);
/// 等待1s
Sleep(1000);
//ReleaseMutex(hMutex);
/// 测试时发现即使在func1中没有放锁,func2也是可以对g_iTotal进行操作,这不是想要的结果,期待的情况应该是func2也是无法对g_iTotal进行操作,才是互斥。但微软提供的文档示例是可以用Mutex实现互斥的:https://msdn.microsoft.com/en-us/library/windows/desktop/ms686927(v=vs.85).aspx
/// Mutex的互斥效果没有问题!是因为func1退出了,锁才被释放,func2等待func1退出之后,就获得了锁,从而才能对g_iTotal进行操作。
/// 验证:等待输入字符,func1一直没有退出,可以发现func2的确一直在等待(此时是死锁),直到按下任意键后,func1退出,func2才能对g_iTotal进行操作。所以Mutex的互斥效果是没有问题的
//_getch()
}
return 1;
}
UINT WINAPI func2(LPVOID arg)
{
/// 同上
for (int i=0;i<5;i++)
{
WaitForSingleObjeat(hMutex,INFINITE);
g_iTotal++;
printf("t2:%d\n",g_iTotal);
ReleaseMutex(hMutex);
}
return 1 ;
}
CloseHandle(hArray[0]);
CloseHandle(hArray[1]);
/// 关闭Mutex
CloseHandle(hMutex);
/// 售票员和司机的例子
/// 这里用到了WIN32的函数,需要把这些头文件包含进来
#include
#include
#include
/// 创建一个Event,Event是一个HANFDLE类型
HANDE g_hEvent = NULL;
/// 创建一个、Event来进行同步
g_hEvent = CreateEvent(NULL,FALSE,NULL);
/// 创建两个线程,Driver,Seller
unsigned tid = 0;
HANDLE hArray [2] = {0};
hArray[0] = (HANDLE)_beginthreadex(NULL,
0,
Driver,
NULL,
0,
&tid);
hArray[1] = (HANDLE)_beginthreadex (NULL,
0,
Seller,
NULL,
0,
&tid);
UINT WINAPI Driver(LPVOID arg)
{
/// 如果当前有其他线程在堆栈进行操作时,则等待该线程操作完栈
for (int i=0;i<5;i++)
{
printf("driver:waiting to driver...\n");
/// 等待seller卖完票
WaitForSingleObjeat(hEvent,INFINITE);
printf("driver:begin to driver\n");
printf("driver:bus goes\n");
}
return 1;
}
UINT WINAPI Seller(LPVOID arg)
{
for (int i=0;i<100;i++)
{
printf("seller:%d ticket sold\n",i);
/// 等待1s
Sleep(1000);
}
printf("seller:ticket finished,close the door\n");
/// seller卖完票了,通知Driver关门开车
SetEvent(g_hEvent);
return 1 ;
}
CloseHandle(hArray[0]);
CloseHandle(hArray[1]);
/// 关闭Mutex
CloseHandle(hEvent);
名字
/// 创建一个共享内存,A进程与B进程想通过共享内存进行进程间通信,必须要保证共享内存的名字要一致
TCHAR szName[]=TEXT("Global\\MyFileMappingObject");
/// 用共享内存创建一个句柄
hMapFile = CreateFileMapping(szName..);
/// 拿到共享内存的起始地址,即把共享内存映射到自己的进程空间中
pBuf =(LPTSTR)MapViewROfFile(hMapFile... )
/// 得到地址之后就在进程中对共享内存读/写
Read/Write
/// 当进程结束对共享内存的访问之后,就需要释放掉共享内存,即把指针和共享内存的映射关系去掉
UnmapViewOfFile(pBuf);
/// 把共享内存的句柄关闭
CloseHandle(hMapFile);
/// 指定管道的名字,"\\\\.\\Pipe\\"是固定的
#define PIPE_NAME"\\\\.\\Pipe\\test"
/// 创建管道
CreateNamedPipe()
/// 连接管道
ConnectNamedPipe()
//读/写操作
WriteFile()
ReadFile()
ConnectNamedPipe()
等待连接CreateFile()
连接服务进程/// 创建匿名管道
BOOL WINAPI CreatePipe(
_Out_ PHANDLE hReadPipe,
_Out_ PHANDLE hWritePipe,
_In_opt_ LPSECURITY_ATTRIBUTES IpPipeAttributes,
_In_ DWORD nSize );
ReadFile/WriteFile
管道1 管道2
父进程 尾部:hChildStdinWr 头部:hChildStdoutRd
| /\
数据流动方向(半双工) (1) |
| (2) (3)
\/
—> |
子进程 头部:hChildStdinRd 尾部:hChildStdoutWr
/// A
/// 创建一个信号量,
HANDLEsemaphore CreateSemaphore(NULL,
1, ///< 初始计数,表示可用资源个数
2, ///< 最大计数,表示最多可用资源个数,如果为1,退化成互斥体
_T("Global\\TestSemaphore")); ///< "Global"表示跨会话(操作系统支持多用户登陆,每一个用户登陆就是处于一个会话当中),即其他会话可以访问当前会话中创建的信号量
/// B
/// 通过信号量名字打开它
HANDLE semaphore = OpenSemaphore(
SEMAPHORE_ALL_ACCEss,
FALSE,_T("Globa\\TestSemaprhore"));
/// 等待计数,没有资源可用则睡眠等待,这个函数可以等待信号量,互斥体,事件
WaitForSingleObject(semaphore,INFINITE); ///< 对计数减一
/// ...使用共享资源
/// 使用完对计数加一
ReleaseSemaphore(semaphore,1,NULL);
利用信号量启动单实例程序
m_hSem = CreateSemaphore(NULL,1,1,AfxGetApp()->m_pszAppName);
/// 信号量已存在?
///信号量存在,则程序己有一个实例运行
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
/// 关闭信号量句柄
CloseHandle(m_hSem);
m_hsem =.NULL;
/// MessageBox(NULL,L"程序已经运行",L"Error",MB_OK);
HWND hWnd = ::FindWindow(NULL,_T("PopupClient"));
if(hvVnd)
{
::setForegroundWindow(hWnd);
::ShowWindow(hWnd,SW_SHOW);
}
/// 前一实例已存在,但找不到其主窗
/// 可能出错了
/// 退出本实例
return FALSE;
}
无锁化编程
(Lock-Free):不使用锁的情况下实现多线程之间对变量进行同步和访问的一种程序设计实现方法,Lock-Free的程序不包括锁机制,但不包括锁机制的程序不一定是lock-free的。
同步编程
:线程在调用函数的时候,只要这个函数没有拿到数据,就一直在哪里等待,不会立即返回,直到拿到数据才会返回。同步阻塞编程
(Block)
同步非阻塞编程
(Non-blocking Synchronization) :根据粒度不同分为以下几类
Wait-free
:所有线程
可以在有限步之内结束Lock-free
:任何时刻至少有一个线程
可以在有限步内完成操作。Obstruction-free
:一个线程,在其它线程都暂停(无竞争)的情况下,可以在有限步之内结束。死锁
活锁
饥饿
:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
先来先服务
)优先级翻转
:T2如果优先级高,就会造成优先级翻转,造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。解决方法:优先级天花板
;优先级继承
等。性能下降
互斥条件
:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求著只能等待,直至占有资源的进程用毕释放。请求和保持条件
:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源己被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。(A,B两把锁拿和放的顺序:所有进程都应该按照:拿A,拿B,放B,放A的顺序)不剥夺条件
:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。环路等待条件
:指在发生死锁时,必然存在—个进程程-资源的环形链
,即进程集合P0,P1,P2,…, Pn}中的P0正在等待一个P1占用的资源:P1正在等待P2占用的资源,…, Pn正在等待已被P0占用的资源。
!deadlock
分析哪个地方发生死锁!locks
列举进程中锁的使用情况,应用层和内核层都可以用
~
查看是进程中的哪一个线程占用~n kb
看占用锁线程的栈,画出进程程-资源请求图
看是否形成环@todo
悲观锁
:总做最坏的打算(悲观),每次访问数据时都认为别人会修改,所以每次在拿数据的时候都会上锁
,这样其他人想访问这个数据就会阻塞直到获得该锁为止关系型数据库里的行锁,表锁等,读锁,写锁等,都是悲观锁,即在做操作之前先上锁Java里面的同步原语synchronized
和ReentrantLock
的实现也是悲观锁select" from mytable `for update`
public syhchronized void func(){...}
乐观锁
:每次访问数据的时候都认为别人不会修改(乐观),所以不上锁.乐观锁的缺点是不能解决脏读
的问题,所以在更新时需要判断此期问有没有别人更新这个数据需要其他机制辅助,比如使用版本号等机制。乐观锁适用于多读
的应用类型,这样可以提高效率。
并发量不大且不允许脏读
,可以使用悲观锁解决并发问题。但如果系统的并发非常大
的话,悲观锁定会带来非常大的性能问题,这个时候我们就要选择乐观锁。Atomic
原子操作,针对计数器,可以使用原子加,比如Atomiclnteger_sync_fetch_and_add
CAS
(Compare and Swap)
CMPXCHG
指令,比较看内存的值有没有被修改,如果发现值在这段时间没有被修改,就可以放心去修改这个值Ring Buffer
环形队列
RCU
(Read-Copy-Update) ,新旧副本切换机制,对于旧副本可以采用延迟释放的做法。 /// i++对应三条汇编指令,所以i++执行中间是可以被打断的
/// 读、改、写(回)
/// AT&T汇编,赋值方向是从左往右
movl i,%eax
addl $1,%eax
movl %eax,i
原子操作
:在执行完毕之前不会被任何其它任务或事件中断的一系列操作它是非阻塞编程
的基础,没有原子操作,会因为中断异常等各种原因
引起数据状态的不一致从而影响到程序的正确。CMPXCHG
,就是一种CAS
原子操作,属于Read-Modify-Write (RMW)l类型指令重排
,内存顺序冲突
(Memory order violation) 等问题。原子性
确保指令执行期间不被打断,要么全部执行,要么根本不执行顺序性
确保即使两条或多条指令出现在独立的执行线程中或者独立的处理器上时,保持它们本该执行的顺序伪代码:
x=y=0;
线程1:
x=1;
r1=y;
线程2:
y=2;
r2=x;
乱序执行
的情况下: Core1的指令预处理单元看到线程1的两条语句没有依赖性
(不管哪条语句先执行,在两条指令语句完成后都会得到一样的结果),会先执行r1=y再执行x=1,或者两条指令同时执行。Core2也一样。这样一来就有可能出来个r1=r2=0的结果。synchronized-with
机制happens-before
机制i = 0;
i = 1; ///< 线程A执行
j = i; ///< 线程B执行
j是否一定等于1呢?假定线程A的操作(i=1)happens-before线程B的操作(j=i) ,那么可以确定线程B执行后即使在乱序等情况下j=1一定成立
原子性
和synchronized-with
和happens-before
机制有不同的定义/// 再宽松内存模型下只保证原子性,不保证顺序性
auto r1 = y.load(std::memory_order_relaxed);
x.store(r1, std::memory_order_relaxed);
CAS
(Compare and Swap,即比较并替换),wikipedia中对于CAS的定义为:
CAS
操作包含三个操作数s内存地址
(V)、预期原值
(A)、新值
(B)。如果内存地址的值与预期原值相同,那么处理器会自动将内存的值更新为新值。否则,处理器不做任何操作。无论哪种情况,处理器会在CAS指令之前返回该地址的值
。CAS有效地说明了"我认为地址V应该包含值A;如果包含该值,则将B放到这个地址;否则,不要更新该地址,只告诉我这个地址现在的值即可"
/// CAS封装成一个函数,整个过程是原子操作,不可以被打断
/// CAS封装程一个函数,但函数里面有这个多条指令,如何能保证整个函数是原子操作呢?
/// CAS是系统原语,CAS操作是一条CPU的原子指令,所以不会有线程安全问题
/// 在JDK1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了。
bool compare_and_swap(int *reg,int oldval,int newval)
{
int reg_val = *reg; ///< 把内存地址的值读出
/// 轮询直到成功才返回
while(1){
if(reg_val == oldval) ///< 把读出的值和期望的值做比较
{
*reg = newval; ///< 假设读出的值和期望的值一致,认为没有人对当前的地址对应的内存进行修改,把新值写入当前内存
return true;
}
}
return false;
}
几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是CMPXCHG
汇编指令
自旋
,那么长时间循环会导致CPU开销很大;Linux的CAS
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval,...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newal,...)
```
- 2) `Windows的CAS`
```c
InterlockedCompareExchange(
__inout LONG volatitle *Target,
__in LONG Exchange,
__in LONG Comperand);
C++11中的CAS
/// C++11中的STL中的atomic类的函数可以跨平台,但底层还是根据不同系统,调用系统的CAS
template class
bool atomic_compare_exchange_weak(std:atomic *obj,T* expected,T desired);
template class
bool atomic_compare_exchange_weak(volatile std::atomic *obj,T* expected,T desired);
Atomic
的类的底层原理都是CAS。在java.util.concurrent
包中大量实现都是建立在基于CAS实现Lock-Free
算法上,没有CAS就不会有此包。java.util.concurfent.atomic
提供了基于CAS实现的若干原语/// 原子类型整型类,初始值是5
Atomiclnteger atomiclnteger = new Atomiclnteger(5);
/// CAS操作,把值改为10
atomiclnteger.compareAndSet(5,10);
/// getAndAddInt的实现
public final int getAndAddInt(Object obj, long obj_addr,int val) {
int old;
do{
old = this.getIntVolatile(obj,obj_addr);
}while(!this.compareAndSwapInt(obi,obj_addr,old, old + val)); ///< CAS
return old;
}
/// incrementAndGet的实现
public final int incrementAndGet(){
for (;;){
int current = get();
int next = current + 1;
/// CAS
if(compareAndSet(current,next))
{
return next;
}
}
版权所有,```
##### Java基于CAS的应用2:自旋锁
- 自旋锁也用到了CAS
```java
/// 自选锁的实现
public class SpinLock
{
/// 拥有自旋锁的线程的句柄或者id或者对象
private AtomicReference<Thread> owner = new AtomicReference<>();
/// 次数,表示自旋锁可以被同一线程可以递归地获取多少次
private int count =0;
/// 加锁
public void lock()
{
/// 获取当前想要获取这把自旋锁的的线程
Thread current = ThreadcurrentThread();
/// 如果该线程已经拥有这把自旋锁,统计,然后直接返回
if(current == owher.get())
{
count++;
return;
}
/// 如果该线程没有拥有这边自旋锁,调用CAS,如果为NULL(当前没有线程占用这把自选锁),则设置为current的线程,否则就在while轮询等待,直到拿到锁
while(!owner.compareAndSet(null,current)){}
}
/// 同上
public void unlock()
{
Thread current = Thread.currentThread();
if(current == owner.get()
{
if(count!=O)
{
count--;
}
else
{
owner.compareAndSet(current,null);
}
}
}
}
synchronized
锁升级原理在锁对像的对象头
里面有一个threadid
字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁
(实际上没有加锁,只是标记了一下),并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁
,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有获取到要使用的对象,此时就会把锁从轻量级升级
重量级锁
(进入系统内核,放入等待队列,由操作系统安排),此过程就构成了synchromized锁的升级。
性能消耗
。在Java 6之后优化 synchronized的实现方式,使用了偏向锁
升级为轻量级锁
再升级到重量级锁
的方式,从而减低了锁带来的性能消耗。Lock-Free
的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
ABA
问题:如果内存地址V初次读取的值是A
,在CAS等待期间它的值曾经被改成了B
,后来又被改回为A
,那CAS操作就会误认为它从来没有被改变过(手提箱例子,把手提箱的数据读取,然后再把手提箱放回去)。ABA问题以及解决:使用带版本号的原子引用AtomicStampedRefence
/// 不带版本号
AtomicReference<String> atomicReference = new AtomicReference<>("A");
/// 带版本号
AtomicStampedReference <String> stampReference = new AtomicStampedReference<>("A",1);
/// 线程1:
/// 有版本号的初始版本是1
int stamp = stampReference.getStamp( );
/// 无版本号的原子操作,先把A设置变成B
atomicReference compareAndSet("A","B");
/// 无版本号的原子操作,先把B设置变成A
atomicReference compareAndSet("B","A");
/// 无版本号的原子操作,先把A设置变成B,stamp+1
stampReference.compareAndSet("A","B",stamp,stamp + 1);
/// 无版本号的原子操作,先把A设置变成B,stamp再+1
stampReference.compareAndSet("B","A" ,stamp+ 1,stamp+ 2);
/// 有版本号的最终版本是3
stampReference.getStamp();
线程2:
/// 拿到版本号是1
int stamp = stampReference.getStamp( );
/// 无版本号的原子操作,先把A设置变成C,因为它不知道中间A变成了B又恢复成A,所以操作可以执行
atomicReference.compareAndSet("A","C");
/// 拿到的值是C,说明该操作成功了
atomicReference.get( );
/// 线程2 的版本号是1,而线程1已经把版本号改成3了,1!=3,操作无法执行
stampReference compareAndSet("A","C",stamp,stamp + 1);
/// 拿到的值还是原来A,说明该操作失败了
stampReference getReference( );//A
next==NULL
/// 出队
/// 如果队列为空,直接返回出队失败
/// 否则,修改front指向待出队结点的下一结点,返回出队结点的值,删除出队的结点
int cas_deque(LinkQue *q, int *e)
{
QNode *tmp=NULL;
do
{
if (is_que_empty(q))
{
return(O);
}
/// 拿到front的下一个结点
tmp = q->front->next;
}
while(!CASH(long *)(&(q->front->next)),(long)tmp,(long)temp->next)); ///< 用CAS把结点摘掉,即修改front指向待出队结点的下一结点
*e = tmp->data;
free(temp);
returnt 1;
}
/// 入队
/// 创建一个结点
/// 把结点插入到rear的后面
/// 把rear指向新的结点
void cas_enque(LinkQue *q, int e)
{
/// 创建一个结点
QNode newNode = (QLode *)malloc(sizeof(QNode));
newNode->data = e;
newNode->next = NULL;
QNode *tmp;
do
{
tmp = q->rear;
}
while(!CAS((long *)(&(tmp->next)),NULL, (long)newNode)); ///< 把结点插入到rear的后面
/// 把rear指向新的结点
Pq->rear = newNode;
}
写入索引
(或指针,rear)只允许生产者访问并修改,只要写入者在更新索引之前将新的值保存到缓冲区中,则读者将始终看到一致的数据。读取索引
(front)也只允许消费者访问并修改。int EnQueue(int value); ///< rear = (rear+1) % MAXSIZE;
int DeQueue(int *value); ///< front = ( front+1) % MAXSIZE
int IsQueueFull(); ///< (rear+1) % MAXSIZE == front
int IsQueueEmpty(); ///< rear==front
2的幂
。读、写指针分别是无符号整型变量
。
指针值 & (缓冲区长度 - 1)
。比求余
操作高效。读指针 + 缓冲区存储的数据长度 = 写指针
len = min{待写入数据长度,缓冲区长度 - (写指针-读指针)}
防止溢出RCU
(Read-Copy-Update) 主要针对链表(堆上分配的内存),读取数据时不对链表进行加锁,允许多个线程同时读取,而只能一个线程对链表进行修改(加锁),在Linux内核中有广泛应用读多写少
,比如浏览电脑的目录RCU的实现
需要考虑:
宽限期
:在读取一个节点过程中,另外一个线程删除了这个节点。删除线程可以把这个节点从链表中移除(remove),但不能立即销毁它(free),必须等到所有的读取线程完成后。这个过程称为宽限期(Grace period)。节点完整性
:如果读线程读到了另一线程插入的一个新节点,需要保证读线程读到的这个节点的完整性。链表完整性
:写线程新增或者删除一个节点,不会导致遍历一个链表从中间断开。但并不保证一定能读到新增的节点或者不读到要被删除的节点。/// 伪代码
node *g_data;
DEFINE_SPINLOCK(mutex};
/// 读线程
void work_read(void)
{
node *fp = g_data;
/// 如果读线程执行到此处,时间片到了被切换出去,写线程把这个节点修改或者删除释放掉了,读线程再切换回来,继续往下执行会出问题,数据被修改了或者访问一个无效内存奔溃
if( fp != NULL )
{
dosth(fp->a,fp->b,fp->c);
}
}
/// 写线程
void work_update(node* new_fp)
{
/// 拿到锁
spin_lock(&mutex);
node *old_fd = g_data;
g_data = new_fp;
/// 释放锁
spin_unlock(&mutex);
}
/// 伪代码
void work_read(void)
{
rcu_read_lock(); ///< 帮助检查宽限期是否结束
nodeNfp = g_ data;
if( p!= NULL) ///< 此处切换
{
dosth(fp->a, fp->b, fp->c);
}
rcu_readunlock();
}
void work_ update(node * new_ fp)
{
spin_lock(&mutex);
node *old_fp = g_data;
g_data = new_fp;
spin_unlock(&mutex);
synchronize_rcu(); ///< 宽限期开始,等待所有线程rcu_read_unlock();
kfree(old_fp);
}
/// 读线程
void work_read(void)
{
rcu_read_lock(); ///< 帮助检查宽限期是否结束
node *fp = g_data;
///< 由于程序的乱序执行,有可能这部分指令先于rcu_read_lock()执行,情况1:fp有可能是之前指向的老节点的数据
/// 情况2:有可能有一部分指向老节点,而有一部分指向新节点的数据,因为work_update会修改g_data = new_fp
if( fp != NULL )
{
dosth(fp->a, fp->b, fp>c);
}
rcu_read_unlock();
}
/// 写线程
void work_update(node * new_fp)
{
spin_Lock(&mutex);
node *old_fp = g_data;
new_fp->a =...
new_fp->b =...
new_fp->c =...
g_data = new_fp; ///< 乱序执行,可能会先于前面new_fp->b等赋值指令,与读线程结合,有可能读线程访问的时候,a初始化了,c也初始化了,但b没有初始化,这也是节点的不完整性,情况3
spin_unlock(&mutex);
synchronize_rcu(); ///< 宽限期开始,等待所有线程
kfree(old_ fp);
}
/// 问题1: work_read(void)乱序执行,导致部分数据old和new
node *fp = rcu_dereference(g_data); ///<替换node *fp= g_data,即订阅系统
/// 问题2:work_update乱序执行,导致部分数据初始化和部分数据未初始化
rcu_assign pointer(g_data,new_fp); ///< 替换g_data = new_fp; ,保证执行顺序,即发布系统
消耗一样很高
(类似spin_ lock)。无锁算法及相关数据结构并不意味在所有的环境下都能带来整体性能的极大提升。循环CAS操作对时会大量占用cpu,对系统时间的开销也是很大。这也是基于循环CAS实现的各种自旋锁不适合做操作和等待时间太长
的并发操作的原因。而通过对有锁程序
进行合理的设计和优化,在很多的场景下更容易使程序实现高度的并发性。队列、栈、链表、词典
等/// 加锁与批处理
lock();
/// 当循环越大,与原子操作相比,加锁程序的系统性能越强
for(k=0;k<100;k++)
i++;
unlock();
/// CAS原子操作,没办法优化批处理
for(k=0;k<100;k++)
InterlockedIncrement(i);
因此如果纯粹希望通过使用CAS无锁算法及相关数据结构而带来程序性能的大量提升是不可能的
,硬件级原子操作使应用层操作变慢,而且无法再度优化。相反通过对有锁多线程程序的良好设让人可以使程序性能没有任何下降,可以实现高度的并发性。死锁
、优先级反转
等问题,因此在对应用程序不太复杂
,而对性能要求稍高
时,可以采用有锁多线程。而程序较为复杂
,性能要求满足使用
的情况下,可以使用应用级无锁算法MidiShare
Source Code is ayailable under the GPL license. MidiShare includes implementations of lock free FIFO queues and LIFO stacks.Appcore
is an SMP andPHyperThiread friendly library whicbuises L .ock- free techniques to implement stacks, queues, linked lists and other useful data structures. Appcore appearscurrently to be forx86 computers running Windows. The licensing terms of Appcore areextremely unclear.Noble
- a ibraryof non-blocking synchronisation protocols. Implements lock free stack,queue,singlylinkedist, snapshots and registers Noble is distributed under a licensewhich onlypermits non-commercial academicusek(commercial version)lock-free-lib
published under the GPLlicensex Includes implementations gfsoftwaretransactional memory, multi-workd/CAS primitives, skip lists, binary search trees,and red-black trees. For Alpha, Mips,ia64}x86,PPC, and Sparc.Nonblocking multiprocessormultithread algorithms in C++
(for MSVCIx86) postedQprof
includes the Atomic ops library of atomic operations anddata structures under an MIT-style license. Only available for Linux at the moment, but there are plans to support other platforms. download available hereAmino Concurrent Building Blocks
provides lock free. datastructures and STM for C++ and Java under an Apache Software (2 .0) licence.