Windows 线程同步

本文详细介绍了线程同步的各种机制,包括用户模式下的原子访问、关键段、读写锁等,以及内核对象如信号、计时器、信号量和互斥量的使用方法,并探讨了如何避免死锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    当多个线程访问同一个共享资源时,为了保证数据的完整性,引入了线程同步机制。

一、用户模式下的线程同步

1、原子访问

    CPU平台支持原子访问。如果是x86系列CPU,那么Interlocked系列函数会在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一个内存地址。我们必须保证传给Interlocked系列函数的变量地址是经过对其的,否则这些函数可能会失败。

   Interlocked系列函数(包括普通的数值类型,普通的算术运算)

   还有对单项链表的栈操作:   InitializeSListHead,   InterlockedPushEntrySList,   InterlockedPopEntrySList,   ... ...


2、关键段 critical section 

    易用、快速(内部使用interlocked系列函数),但是无法在多个进程的线程间进行同步

    一个EnterCriticalSection函数必须有一个LeaveCriticalSection函数与之对应,否则一个线程会一直独占了某些资源,即使该线程结束之后,这些资源也被关键代码段锁定。而其他线程如果调用EnterCriticalSection进入该关键代码段是无法访问这些资源的。

 如果一个关键代码段已经被其他线程所拥有,那么如果当前线程试图进入这个关键代码段的时候,会立即被设置为等待状态。意味着该线程必须从用户模式转入内核模式,大约需要1000个CPU周期。这种转换是需要付出代价的。实际上,在多CPU计算机上,当前拥有资源的线程可能正执行在另一个CPU上,这样,它很可能会马上离开关键代码段,释放相关资源,所以可以使用关键段+自旋锁。

CRITICAL_SECTION,InitializeCriticalSetion

Entercriticalsection/ TryEnterCriticalSetion

Leavecriticalsection,Deletecriticalsection  

3、关键段和旋转锁

    先在用户态等待,然后再陷入内核等待,比关键段更快速

    如果两个或两个以上线程在同一时刻争夺同一个关键段,那么关键段会在内部使用一个事件内核对象。当内存不足的情况下,可能创建事件内核对象会失败,这个时候EnterCriticalSection会抛EXCEPTION_INVALID_HANDLE异常,这时候需要作一些特殊处理。要注意的情况是,如果在单CPU的计算机上,该函数的第二个参数dwSpinCount会被忽略,永远为0,因为在单CPU上,如果一个线程在循环尝试请求资源,而当前拥有资源的线程不可能被调度,资源是无法释放的。

InitializeCriticalSetionAndSpinCount

SetCriticalSetionSpinCount


4、读写锁

    适用于生产者消费者模型,它可以区分读取资源的线程和更新资源的线程

    SRWLock,InitializeSRWLock,AcquireSRWLockExclusive,ReleaseSRWLockExclusive

5、条件变量

    如果读取者线程没有数据可以读取,那么它应该将锁释放并等待,直到写入者线程产生了新的数据为止。如果用来接收写入者线程产生的数据结构已满,那么写入者同样应该释放SRWLock并进入睡眠状态,直到读取这线程把数据结构清空为止。当线程检测到相应的条件满足的时候(比如,有数据供读取者使用),它会调用 WakeConditionVariable 或  WakeAllConditionVariable,这样阻塞在Sleep*函数中的线程就会被唤醒。

     条件变量一般是配合其他同步机制使用的。例如在生产者线程离开关键段的时候,WakeAllConditionVariavble;当消费者进入关键段,发现没有东西可读时,可以使用SleepConditionVariable。

    SleepConditionVariableSRW,SleepConditionVariableCS,WakeConditionVariable, WakeAllConditionVariable

6、高速缓存行

    多CPU情况下,CPU的高速缓存行,可能会造成数据不完整性。我们可以利用CPU的这个特性,来组织数据结构,把数据和缓存行的边界对齐,把经常修改的数据和只读数据分开存放。

7、一些技巧

(1)以独占方式操作一组对象时使用一个锁,不要为每个对象都创建一个锁;

(2)同时访问多个逻辑资源,有多个锁时,一定要按照顺序来获取 锁;

(3)不要长时间占用锁;

二、使用内核对象进行线程同步

    WaitforXXX 副作用:一个“自动重置”的事件内核对象收到通知,转变为已通知状态的时候,最多只能唤醒“一个”等待在它上的线程。一个“人工重置”的事件内核对象收到通知,转变为已通知状态的时候,能够唤醒“所有”等待在它上的线程。

1、信号

CreateEvent

SetEvent 有信号

ResetEvent 无信号

palseEvent 先出发信号,马上回到无信号状态

2、计时器

    隔多长时间触发,内核对象变成触发状态。

CreateWaitableTimer,OpenWaitableTimer,CancleWaitableTimer

3、信号量

    如果当前资源计数大于0,那么信号量处于触发状态,=0,为未触发状态,系统绝对不会让当前资源计数变成0,当前资源计数绝对不会大于最大资源计数。releaseSemaphore会增加当前资源计数。

CreateSemaphoreEx

OpenSemaphore

ReleaseSemaphore

4、互斥量

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa,BOOL fInitialOwner ,LPTSTR lpszMutexName);

    fInitialOwner表示创建互斥量的线程是否是该互斥量的最初拥有者。如果为TRUE,表示线程将拥有它所创建的这个互斥量,因此互斥量将处于无信号状态,只有创建该互斥量的线程此时可以访问互斥量保护的数据,而其它等待这个互斥量的线程都将被挂起,等待着互斥量变成有信号状态之后它们才能继续执行,否则就在那里等着,也就是在那里睡觉。lpszMutexName为所创建的互斥量起一个名字,这个名字用于在多个进程间共享互斥量。

    互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。也能完美的解决某进程意外终止所造成的“遗弃”问题。

    互斥量是属于线程的,如果使用ExitThreadTerminateThread来强行退出线程,互斥量没有被释放,不过系统会将这个遗弃的互斥量交给正在等待这个互斥量的对象。

    CreateMutexEx,OpenMutex,ReleaseMutex

三、死锁检测

GetThreadWaitChain

OpenThreadWaitChainSession



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值