同步对于计算机系统来说是一个必须要面对的问题, windows系统对内核或外部应用程序提供了多种同步机制,通过这篇文章来介绍
- 同步机制的工作机制
- 同步机制的使用方法
同步机制可以按照不同的方式分类,如按照IRQL的高低,按照内核模式还是用户模式,按照是否可以跨进程等。在这里我们按照是否升高IRQL来将同步机制分类。
高IRQL同步
系统提供的同步机制必须要保证一点,就是在任意时刻只能由一个处理器在一个需要同步的关键段执行,那在这个过程中最大的问题来自于系统中断,如果处理器在执行关键段的时候被中断打扰,就无法保证同步。CPU可以通过提升IRQL的方式来屏蔽中断,但是这种方式只适用于单个内核,因为每个内核会有一个IRQL。对于多核处理器不仅需要提升IRQL,同时还要采用其他方式避免其他内核同时对关键段的访问。
Interlocked operations
工作机制
Interlocked operations提供了一系列的方法可以原子性的对变量进行更改操作。其实现方式是通过硬件支持的指令锁住处理器总线来避免其他处理器访问。
通过debug命令可以查看该系列方法的汇编代码。
0:010> uf kernel32!InterlockedIncrementkernel32!InterlockedIncrement:
75 7619efc0 8b4c2404 mov ecx,dword ptr [esp+4]
78 7619efc4 b801000000 mov eax,1
79 7619efc9 f00fc101 lock xadd dword ptr [ecx],eax
80 7619efcd 40 inc eax
81 7619efce c20400 ret 4
这类方法在windows编程中经常会被用到,例如CLR对于对象头的同步块处理中去设置某些位的值,由于需要多线程同步,就是通过调用类似的方法实现。
使用方法
同时C#中也包装了类似的方法
http://msdn.microsoft.com/en-us/library/system.threading.interlocked.aspx
Spinlocks
工作机制
Spinlocks是一种内核在访问内部数据结构(例如DPC Queue)的时候使用,它与其要保护的数据结构一样都是在non-paged pool内存中分配。他提供的同步方式是,当线程尝试去得到锁的过程中如果锁被其他线程占用,则不停的循环访问该锁,直到得到为止。这种机制一般用在等待时间很短的情况下,否则这种忙等待机制会给CPU增加非常大的负担。
它的实现也是通过硬件支持的test-and-set指令实现。与interlocked operations效果类似,锁住处理器总线避免其他处理器同时访问,另外也要提升IRQL避免可能的中断打扰。
使用方法
C#中提供了类似的实现
http://msdn.microsoft.com/en-us/library/system.threading.spinlock.aspx
Queued spinlocks
工作机制
相较常规的spinlock一种更加常用的spinlock叫做queued spinlock。根据名字可以推测应该有一个queue与spinlock相关联。当处理器请求一个被锁住的spinlock时,处理器把自己的标识放在spinlock相关联的queue中,当拿着锁的处理器释放该锁时,他会把锁的拥有权交给queue中的一个排队的处理器。这样处理器就不需要去查看锁的状态而只需要查看当前处理器的一个标志位来判断是否轮到了自己。
Queued spinlock与常规的spinlock有两点不同,
第一是多处理器总线不会被频繁的访问
第二是queued spinlock对于锁的取得是有顺序的
使用方法
Windows定义了一定数量的queued spinlocks,可以通过调用KeAcquireQueuedSpinLock来获得。注意这种方式只是给windows内核自己使用的。第三方的开发者无法使用这种方式。但是不要失望,接下来就是我们可以采用的方式。
Instack queued spinlocks
工作机制
驱动开发者可以使用动态分配的queued spinlocks,系统组件中例如cache manager,executivepool manager, NTFS都用到了这种方式。因为相较于windows的静态queued spinlocks动态分配的spinlock需要在stack上保存spinlock的handle以及相应的queuehandle,所以我们称它为instack queuedspinlocks。
使用方法
Instack queued spinlocks可以通过以下方式在内核模式下使用
http://msdn.microsoft.com/en-us/library/windows/hardware/ff559970%28v=vs.85%29.aspx
Executive interlocked operations
工作机制
基于spinlock内核还提供给我们了一系列比较高级的同步操作,比如在单链表或者双链表中添加或者删除节点。
例如单链表可以用ExInterlockedPopEntryList,ExInterlockedPushEntryList,双链表可以用
ExInterlockedInsertHeadList,ExInterlockedRemoveHeadList,而这些方法均需要一个标准的spinlock作为参数。
使用方法
对于此种方式windows在内核模式和用户模式均提供了相应的数据结构来实现。
Kernel mode
http://msdn.microsoft.com/en-us/library/windows/hardware/ff563802%28v=vs.85%29.aspx
User mode
低IRQL同步
Spinlocks不能够完全满足系统对同步机制的需要,由于等待一个spinlock相当于在浪费一个处理器的资源,spinlock只能在以下条件满足的时候才可以使用,
- 对于资源的访问没有复杂的操作,可以快速释放
- 关键段代码不能被换页出内存,不能引用换页内存数据,不能调用外部过程,不能生成中断或异常
因此为了满足在不同场合的需要,windows提供了以下更丰富的同步机制。
Kernel dispatcher objects
工作机制
Kernel dispatcher objects是通过内核对象来实现的,在任意时刻,一个内核对象只能出去signaled或nonsignaled状态之一,线程只有在它的等待条件满足之后才能恢复运行。这种等待条件的满足只有在其等待的对象状态从nonsignaled转换到signaled时发生。当内核把一个分发对象转换到signaled状态后,接下来检查是否存在线程等待这个对象,如果有的话则将这些线程从等待状态中释放出来,从而恢复了这些线程获得CPU时间片继续执行的权利。
在这里值得一提的是WDK中包含了分发对象的两个重要的数据结构,dispatcher header和wait blocks。线程中包含了指向wait blocks的指针,wait blocks中包含了它在等待的对象的指针,dispacher header属于分发对象,其中包含了在等待该对象的wait blocks链表头,以及当前分发对象的状态。因此根据分发对象我们可以找到有哪些线程的wait blocks正在等待该对象,根据线程也可以找到该线程正在等待哪些对象。
typedef struct _DISPATCHER_HEADER {
union {
struct {
UCHAR Type;
union {
UCHAR Abandoned;
UCHAR Absolute;
UCHAR NpxIrql;
BOOLEAN Signalling;
} DUMMYUNIONNAME;
union {
UCHAR Size;
UCHAR Hand;
} DUMMYUNIONNAME2;
union {
UCHAR Inserted;
BOOLEAN DebugActive;
BOOLEAN DpcActive;
} DUMMYUNIONNAME3;
} DUMMYSTRUCTNAME;
volatile LONG Lock;
} DUMMYUNIONNAME;
LONG SignalState;
LIST_ENTRY WaitListHead;
} DISPATCHER_HEADER;
typedef struct _KWAIT_BLOCK {
LIST_ENTRY WaitListEntry;
struct _KTHREAD *Thread;
PVOID Object;
struct _KWAIT_BLOCK *NextWaitBlock;
USHORT WaitKey;
UCHAR WaitType;
UCHAR SpareByte;
#if defined(_AMD64_)
LONG SpareLong;
#endif
} KWAIT_BLOCK, *PKWAIT_BLOCK, *PRKWAIT_BLOCK;
使用方法
以下对象均为可以被等待的dispatcherobject对应内核分发对象。
Process
Thread
File
Debug Object
Event
Gate
Keyed event
Semaphore
Timer
Mutex
Queue
可以通过调用WaitForSingleObject或WaitForMultipleObjects方法来等待分发对象。
等待方法列表
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686360%28v=vs.85%29.aspx#wait_functions
Fast mutex and guarded mutex
工作机制
Fast mutex通过名字可以推测出来比常规的mutex效率要高,原因在于虽然fast mutex与mutex同样基于内核对象,但是fast mutex只有在出现竞争后才会去等待分发对象,而mutex在每次请求的时都会去请求分发对象。
Guarded mutex在以下三方面实现上的优化性能比fast mutex更胜一筹
避免提升IRQL,内核不需要频繁的去与APIC交互
内部使用同步对象KGATE,Fast Mutex内部使用Event,在获得和释放Gate的逻辑上提供了大量的优化
在没有竞争的情况下取得和释放guarded mutex仅设置一个bit位,而不是像fast mutex失去操作一个整数。
使用方法
如何使用fast mutex和guardedmutex
http://msdn.microsoft.com/en-us/library/windows/hardware/ff545716%28v=vs.85%29.aspx
Executive resources
工作机制
Executive resource是一种在文件系统驱动程序中频繁使用的同步机制,因为它不仅可以提供排他访问也可以以提供共享访问,在请求排它访问的时候内部使用的内核对象为synchronization event,在请求共享访问的时候内部使用semaphore。
使用方法
获取各种类型的访问方式可以通过以下方法。
ExAcquireResourceExclusiveLite
ExAcquireSharedStarveExclusive
Pushlocks
Pushlocks也是建立在gate上面的一种同步机制,比起guarded mutex它更进一步的提供了共享访问和排他访问两种方式,而且大小只有一个指针的大小。
目前只有系统驱动才可以使用pushlocks。
Critical sections
工作机制
Critical sections也是建立在内核分发对象基础上的提供给用户模式下使用的同步机制,他的优越性在于在没有发生竞争的时候不需要进入内核请求分发对象(而实际上99%的情况下对于critical sections的访问是没有竞争的)。另外critical sections可以提供共享和排他访问两种方式。Critical sections的实现也是基于用户模式下调用interlocked operations来设置bit位标识锁是否已经被占用。当另外的线程参与竞争的时候才会进入内核然后将该线程转入等待状态。
使用方法
可以调用以下方法来初始化和使用criticalsection。
InitializeCriticalSectionAndSpinCount
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686908%28v=vs.85%29.aspx
// Global variable
CRITICAL_SECTION CriticalSection;
int main( void )
{
...
// Initialize the critical section one time only.
if (!InitializeCriticalSectionAndSpinCount(&CriticalSection,
0x00000400) )
return;
...
// Release resources used by the critical section object.
DeleteCriticalSection(&CriticalSection);
}
DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...
// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);
// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);
...
return 1;
}
Condition variables
工作机制
Condition variables这种机制是windows提供给用户模式下服务于一种特殊情况的同步方式,即当多线程等待一个变量的值发生变化之后,迅速的将该变量值更改。这里面涉及了两个操作需要原子性的完成,第一是判断变量值变化,第二是重设该变量。
使用方式
可以通过调用InitializeConditionVariable初始化condition variable
请求线程通过调用SleepConditionVariableCS来等待变量值发生变化
服务线程可以通过调用WakeConditionVariable来通知等待线程变量的值发生了变化
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686903%28v=vs.85%29.aspx
DWORD WINAPI ProducerThreadProc (PVOID p)
{
ULONG ProducerId = (ULONG)(ULONG_PTR)p;
while (true)
{
// Produce a new item.
Sleep (rand() % PRODUCER_SLEEP_TIME_MS);
ULONG Item = InterlockedIncrement (&LastItemProduced);
EnterCriticalSection (&BufferLock);
while (QueueSize == BUFFER_SIZE && StopRequested == FALSE)
{
// Buffer is full - sleep so consumers can get items.
SleepConditionVariableCS (&BufferNotFull, &BufferLock, INFINITE);
}
if (StopRequested == TRUE)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Insert the item at the end of the queue and increment size.
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = Item;
QueueSize++;
TotalItemsProduced++;
printf ("Producer %u: item %2d, queue size %2u\r\n", ProducerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a consumer is waiting, wake it.
WakeConditionVariable (&BufferNotEmpty);
}
printf ("Producer %u exiting\r\n", ProducerId);
return 0;
}
DWORD WINAPI ConsumerThreadProc (PVOID p)
{
ULONG ConsumerId = (ULONG)(ULONG_PTR)p;
while (true)
{
EnterCriticalSection (&BufferLock);
while (QueueSize == 0 && StopRequested == FALSE)
{
// Buffer is empty - sleep so producers can create items.
SleepConditionVariableCS (&BufferNotEmpty, &BufferLock, INFINITE);
}
if (StopRequested == TRUE && QueueSize == 0)
{
LeaveCriticalSection (&BufferLock);
break;
}
// Consume the first available item.
LONG Item = Buffer[QueueStartOffset];
QueueSize--;
QueueStartOffset++;
TotalItemsConsumed++;
if (QueueStartOffset == BUFFER_SIZE)
{
QueueStartOffset = 0;
}
printf ("Consumer %u: item %2d, queue size %2u\r\n",
ConsumerId, Item, QueueSize);
LeaveCriticalSection (&BufferLock);
// If a producer is waiting, wake it.
WakeConditionVariable (&BufferNotFull);
// Simulate processing of the item.
Sleep (rand() % CONSUMER_SLEEP_TIME_MS);
}
printf ("Consumer %u exiting\r\n", ConsumerId);
return 0;
}
Slim reader-writer locks
工作机制
通过上面的conditionvariable的实例可以看出它依赖于critical section来提供锁机制,另外一只可供condtion variable依赖的锁就是slim reader-writer lock,这种机制酷似内核中的pushlock,仅有指针的大小,可以提供多个读单个写的访问方式,并且在读与写的访问速度一样快。与pushlock不同之处在于SRW属于用户模式,pushlock供内核使用;SRW不能在共享访问和排他访问之间转换;SRW不能被递归获取。
使用方法
可以通过以下方式获得和释放共享或排他模式的SRW lock
C++
http://msdn.microsoft.com/en-us/library/windows/desktop/aa904937%28v=vs.85%29.aspx
C#
http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx
Run once initialization
工作机制
Run once initialization的出现是为了满足组建加载的时候经常需要做些一次性的工作,例如分配内存,初始化变量等。这些工作经常被放在一个需要线程安全机制保护的方法中,而run once initialization就是为了提供这种原子性的初始化工作而建立的。
系统通过分配一个INIT_ONCE结构来追踪初始化过程的数据和状态信息,线程仅需要调用InitOnceExecuteOnce(同步)或者InitOnceBeginInitialize(异步)来触发初始化过程的执行,同时将INIT_ONCE结构指针作为参数传入,系统内部通过keyedevent来实现线程同步,原子性的更新INIT_ONCE结构来追踪初始化过程是否成功。
使用方法
Run once initialization提供了同步和异步两种调用方式
http://msdn.microsoft.com/en-us/library/windows/desktop/aa363808%28v=vs.85%29.aspx
同步方式
#define _WIN32_WINNT 0x0600
#include <windows.h>
// Global variable for one-time initialization structure
INIT_ONCE g_InitOnce = INIT_ONCE_STATIC_INIT; // Static initialization
// Initialization callback function
BOOL CALLBACK InitHandleFunction (
PINIT_ONCE InitOnce,
PVOID Parameter,
PVOID *lpContext);
// Returns a handle to an event object that is created only once
HANDLE OpenEventHandleSync()
{
PVOID lpContext;
BOOL bStatus;
// Execute the initialization callback function
bStatus = InitOnceExecuteOnce(&g_InitOnce, // One-time initialization structure
InitHandleFunction, // Pointer to initialization callback function
NULL, // Optional parameter to callback function (not used)
&lpContext); // Receives pointer to event object stored in g_InitOnce
// InitOnceExecuteOnce function succeeded. Return event object.
if (bStatus)
{
return (HANDLE)lpContext;
}
else
{
return (INVALID_HANDLE_VALUE);
}
}
// Initialization callback function that creates the event object
BOOL CALLBACK InitHandleFunction (
PINIT_ONCE InitOnce, // Pointer to one-time initialization structure
PVOID Parameter, // Optional parameter passed by InitOnceExecuteOnce
PVOID *lpContext) // Receives pointer to event object
{
HANDLE hEvent;
// Create event object
hEvent = CreateEvent(NULL, // Default security descriptor
TRUE, // Manual-reset event object
TRUE, // Initial state of object is signaled
NULL); // Object is unnamed
// Event object creation failed.
if (NULL == hEvent)
{
return FALSE;
}
// Event object creation succeeded.
else
{
*lpContext = hEvent;
return TRUE;
}
}
异步方式
#define _WIN32_WINNT 0x0600
#include <windows.h>
// Global variable for one-time initialization structure
INIT_ONCE g_InitOnce = INIT_ONCE_STATIC_INIT; // Static initialization
// Returns a handle to an event object that is created only once
HANDLE OpenEventHandleAsync()
{
PVOID lpContext;
BOOL fStatus;
BOOL fPending;
HANDLE hEvent;
// Begin one-time initialization
fStatus = InitOnceBeginInitialize(&g_InitOnce, // Pointer to one-time initialization structure
INIT_ONCE_ASYNC, // Asynchronous one-time initialization
&fPending, // Receives initialization status
&lpContext); // Receives pointer to data in g_InitOnce
// InitOnceBeginInitialize function failed.
if (!fStatus)
{
return (INVALID_HANDLE_VALUE);
}
// Initialization has already completed and lpContext contains event object.
if (!fPending)
{
return (HANDLE)lpContext;
}
// Create event object for one-time initialization.
hEvent = CreateEvent(NULL, // Default security descriptor
TRUE, // Manual-reset event object
TRUE, // Initial state of object is signaled
NULL); // Object is unnamed
// Event object creation failed.
if (NULL == hEvent)
{
return (INVALID_HANDLE_VALUE);
}
// Complete one-time initialization.
fStatus = InitOnceComplete(&g_InitOnce, // Pointer to one-time initialization structure
INIT_ONCE_ASYNC, // Asynchronous initialization
(PVOID)hEvent); // Pointer to event object to be stored in g_InitOnce
// InitOnceComplete function succeeded. Return event object.
if (fStatus)
{
return hEvent;
}
// Initialization has already completed. Free the local event.
CloseHandle(hEvent);
// Retrieve the final context data.
fStatus = InitOnceBeginInitialize(&g_InitOnce, // Pointer to one-time initialization structure
INIT_ONCE_CHECK_ONLY, // Check whether initialization is complete
&fPending, // Receives initialization status
&lpContext); // Receives pointer to event object in g_InitOnce
// Initialization is complete. Return handle.
if (fStatus && !fPending)
{
return (HANDLE)lpContext;
}
else
{
return INVALID_HANDLE_VALUE;
}
}
引用一下
Windows viaC/C++
中对于用户模式下同步机制性能的对比实验结果如下
(
计数单位:微秒
)
线程数 | Volatile Read | Volatile Write | Interlocked Increment | Critical Section | SRWLock Shared | SRWLock Exclusive | Mutex |
1 | 8 | 8 | 35 | 66 | 66 | 67 | 1060 |
2 | 8 | 76 | 153 | 268 | 134 | 148 | 11082 |
4 | 9 | 145 | 361 | 768 | 244 | 307 | 23785 |
参考文档
http://msdn.microsoft.com/en-us/library/windows/desktop/ms686353(v=vs.85).aspx
Windows.Internals 5th - Chapter 3
Windows via C/C++ - Chapter 8 & 9