Windows同步机制原理及使用方法

同步对于计算机系统来说是一个必须要面对的问题, windows系统对内核或外部应用程序提供了多种同步机制,通过这篇文章来介绍

  • 同步机制的工作机制
  • 同步机制的使用方法

同步机制可以按照不同的方式分类,如按照IRQL的高低,按照内核模式还是用户模式,按照是否可以跨进程等。在这里我们按照是否升高IRQL来将同步机制分类。

IRQL同步

系统提供的同步机制必须要保证一点,就是在任意时刻只能由一个处理器在一个需要同步的关键段执行,那在这个过程中最大的问题来自于系统中断,如果处理器在执行关键段的时候被中断打扰,就无法保证同步。CPU可以通过提升IRQL的方式来屏蔽中断,但是这种方式只适用于单个内核,因为每个内核会有一个IRQL。对于多核处理器不仅需要提升IRQL,同时还要采用其他方式避免其他内核同时对关键段的访问。

Interlocked operations

工作机制

Interlocked operations提供了一系列的方法可以原子性的对变量进行更改操作。其实现方式是通过硬件支持的指令锁住处理器总线来避免其他处理器访问。

通过debug命令可以查看该系列方法的汇编代码。

0:010> uf kernel32!InterlockedIncrement
kernel32!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。根据名字可以推测应该有一个queuespinlock相关联。当处理器请求一个被锁住的spinlock时,处理器把自己的标识放在spinlock相关联的queue中,当拿着锁的处理器释放该锁时,他会把锁的拥有权交给queue中的一个排队的处理器。这样处理器就不需要去查看锁的状态而只需要查看当前处理器的一个标志位来判断是否轮到了自己。

Queued spinlock与常规的spinlock有两点不同,

第一是多处理器总线不会被频繁的访问

第二是queued spinlock对于锁的取得是有顺序的

使用方法

Windows定义了一定数量的queued spinlocks,可以通过调用KeAcquireQueuedSpinLock来获得。注意这种方式只是给windows内核自己使用的。第三方的开发者无法使用这种方式。但是不要失望,接下来就是我们可以采用的方式。

Instack queued spinlocks

工作机制

驱动开发者可以使用动态分配的queued spinlocks,系统组件中例如cache managerexecutivepool manager, NTFS都用到了这种方式。因为相较于windows的静态queued spinlocks动态分配的spinlock需要在stack上保存spinlockhandle以及相应的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内核还提供给我们了一系列比较高级的同步操作,比如在单链表或者双链表中添加或者删除节点。

例如单链表可以用ExInterlockedPopEntryListExInterlockedPushEntryList,双链表可以用

ExInterlockedInsertHeadListExInterlockedRemoveHeadList,而这些方法均需要一个标准的spinlock作为参数。

使用方法

对于此种方式windows在内核模式和用户模式均提供了相应的数据结构来实现。

Kernel mode

http://msdn.microsoft.com/en-us/library/windows/hardware/ff563802%28v=vs.85%29.aspx

User mode

http://msdn.microsoft.com/en-us/library/windows/desktop/ms686360%28v=vs.85%29.aspx#singly-linked_list_functions

IRQL同步

Spinlocks不能够完全满足系统对同步机制的需要,由于等待一个spinlock相当于在浪费一个处理器的资源,spinlock只能在以下条件满足的时候才可以使用,

  • 对于资源的访问没有复杂的操作,可以快速释放
  • 关键段代码不能被换页出内存,不能引用换页内存数据,不能调用外部过程,不能生成中断或异常

因此为了满足在不同场合的需要,windows提供了以下更丰富的同步机制。

Kernel dispatcher objects

工作机制

Kernel dispatcher objects是通过内核对象来实现的,在任意时刻,一个内核对象只能出去signalednonsignaled状态之一,线程只有在它的等待条件满足之后才能恢复运行。这种等待条件的满足只有在其等待的对象状态从nonsignaled转换到signaled时发生。当内核把一个分发对象转换到signaled状态后,接下来检查是否存在线程等待这个对象,如果有的话则将这些线程从等待状态中释放出来,从而恢复了这些线程获得CPU时间片继续执行的权利。

在这里值得一提的是WDK中包含了分发对象的两个重要的数据结构,dispatcher headerwait 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

可以通过调用WaitForSingleObjectWaitForMultipleObjects方法来等待分发对象。

等待方法列表

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 mutexmutex同样基于内核对象,但是fast mutex只有在出现竞争后才会去等待分发对象,而mutex在每次请求的时都会去请求分发对象。

Guarded mutex在以下三方面实现上的优化性能比fast mutex更胜一筹

避免提升IRQL,内核不需要频繁的去与APIC交互

内部使用同步对象KGATEFast Mutex内部使用Event,在获得和释放Gate的逻辑上提供了大量的优化

在没有竞争的情况下取得和释放guarded mutex仅设置一个bit位,而不是像fast mutex失去操作一个整数。

使用方法

如何使用fast mutexguardedmutex

http://msdn.microsoft.com/en-us/library/windows/hardware/ff545716%28v=vs.85%29.aspx

Executive resources

工作机制

Executive resource是一种在文件系统驱动程序中频繁使用的同步机制,因为它不仅可以提供排他访问也可以以提供共享访问,在请求排它访问的时候内部使用的内核对象为synchronization event,在请求共享访问的时候内部使用semaphore

使用方法

获取各种类型的访问方式可以通过以下方法。

ExAcquireResourceSharedLite

ExAcquireResourceExclusiveLite

ExAcquireSharedStarveExclusive

Pushlocks

Pushlocks也是建立在gate上面的一种同步机制,比起guarded mutex它更进一步的提供了共享访问和排他访问两种方式,而且大小只有一个指针的大小。

目前只有系统驱动才可以使用pushlocks

Critical sections

工作机制

Critical sections也是建立在内核分发对象基础上的提供给用户模式下使用的同步机制,他的优越性在于在没有发生竞争的时候不需要进入内核请求分发对象(而实际上99%的情况下对于critical sections的访问是没有竞争的)。另外critical sections可以提供共享和排他访问两种方式。Critical sections的实现也是基于用户模式下调用interlocked operations来设置bit位标识锁是否已经被占用。当另外的线程参与竞争的时候才会进入内核然后将该线程转入等待状态。

使用方法

可以调用以下方法来初始化和使用criticalsection

InitializeCriticalSectionAndSpinCount

EnterCriticalSection

LeaveCriticalSection

DeleteCriticalSection

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

AcquireSRWLockExclusive

AcquireSRWLockShared

ReleaseSRWLockExclusive

ReleaseSRWLockShared

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


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值