Loading... # Windows核心编程-学习笔记6 ## 用户模式下的线程同步 线程之间需要通信: * 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 * 一个线程需要通知其他线程某项任务已经完成 ### 原子访问:Interlocked系列函数 **原子访问**:一个线程在访问某个资源同时保证没有其他线程会在同一时刻访问同一资源 简单来说,假设有两个线程都做了x++,x是全局变量,那么有可能结果为2或者1,1的情况是指汇编层面,可能在线程1还没有写入x值时,线程2已经读取了x 为解决这个问题,需要引入Interlocked系列函数,例如x++替换为InterlockedExchangeAdd(&x,1)或者InterlockedIncrement,InterlockedExchangeAdd会返回\*plAddend中原来的值 ~~~c++ LONG InterlockedExchangeAdd(PLONG volatile plAddend, LONG lIncrement); ~~~ Interlocked系列函数原理是在总线上维持一个硬件信号,该信号会组织其他CPU访问同一个内存地址 InterlockedExchange和InterlockedExchangePointer会以原子方式将第一个参数指向的内存地址的当前值替换为第二个参数指定的值。在实现自旋锁时,InterlockedExchange极其有用: ~~~c++ BOOL g_fResourceInUse = FALSE; void Func1() { while (InterlockedExchange(&g_fResourceInUse, TURE) == TRUE) sleep(0); // 访问资源 ... // 不再需要访问资源 InterlockedExchange(&g_fResourceInUse, FALSE); } ~~~ 使用这个要注意自旋锁会耗费CPU时间,需要我们禁用线程优先级的动态提升。此外,必须确保锁变量和锁所保护的数据位于不同的高速缓存中,防止CPU争夺资源影响性能 ~~~c++ PLONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand); LONG InterlockedCompareExchangePointer(PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand); ~~~ 将plDestination指向的值和lComparand值比较,相等则将plDestination指向的值修改为lExchange;InterlockedCompareExchange函数返回\*plDestination原来的值 没有哪个Interlocked函数仅用于读取数值而不修改它,因为没有必要 **volatile**:类型限定符,告诉编译器该变量可能会被应用程序之外的其他东西修改,确切来说告诉编译器不要对这个变量进行任何形式的优化,始终要从变量内存中的位置重新加载变量的值;但是地址变量不用volatile,因为编译器不会优化它 ### 关键段 **关键段**:一小段代码,它在执行前需要独占对一些共享资源的访问权。这种方式可以让多行代码以原子方式操纵资源 **原子方式**:代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统可以暂停当前线程去调用其他线程,但在当前线程离开关键段之前,系统不会调度任何想要访问同一资源的其他线程 分配一个CRITICAL_SECTION数据结构,然后将任何需要访问共享资源的代码放到EnterCriticalSection和LeaveCriticalSection的调用之间,两个函数的调用传递的都是前面数据结构的地址 调用EnterCriticalSection前需要对CRITICAL_SECTION结构的成员使用InitializeCriticalSection进行初始化,一旦进程的线程不再需要访问共享资源,就应该调用DeleteCriticalSection清理CRITICAL_SECTION结构 **EnterCriticalSection在做什么**: * 如果没有任何线程在访问资源,EnterCriticalSection会更新成员变量,表明调用线程已获准对资源的访问并立即返回,允许线程继续执行访问资源 * 如果成员变量表明调用线程已获准访问资源,EnterCriticalSection会更新成员变量,指出调用线程被获准访问的次数并立即返回,线程继续执行,这种情况很少发生 * 如果成员变量表明有一个除调用线程外的其他线程已经获准访问资源,EnterCriticalSection会用一个事件内核对象将调用线程切换到等待状态,系统会记住这个线程想要访问这个资源,一旦当前正在访问资源的线程调用了LeaveCriticalSection,系统会自动更新CRITICAL_SECTION的成员变量并将等待中的线程切换回可调度状态 可以使用TryEnterCriticalSection替代EnterCriticalSection,TryEnterCriticalSection永远不会让调用线程进入等待状态,相反通过返回值表示调用线程是否获准范文资源 **LeaveCriticalSection在做什么**: * 检查结构内部的成员变量并将一个计数器减一,该计数器指出线程调用线程获准访问共享资源的次数 * 如果大于0,直接返回 * 如果等于0,更新成员变量,表明没有任何线程正在访问被保护的资源,同时检查有没有其他线程由于调用了EnterCriticalSection而处于等待状态。如果至少有一个线程正在等待,函数会更新成员变量,将其中一个处于等待状态的线程切换为可调度状态。 为提升关键段的性能,微软将自旋锁合并到了关键段中。只要调用EnterCriticalSection就会用一个自旋锁不断循环,尝试在一段时间内获得对资源的访问权。只有当尝试失败时,线程才会切换到内核模式并进入等待状态(用户模式切换到内核模式开销较大,CPU周期长) 要在使用关键段的同时使用自旋锁,必须调用以下函数来初始化关键段 ~~~c++ BOOL InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount); ~~~ 类似InitializeCriticalSection,第一个参数是关键段结构的地址,第二个参数是我们希望自旋锁循环的次数,次数范围在0到0x00FFFFFF之间任何一个值,在单处理器的机器上调用该函数会忽略第二个参数,因为单处理器上一个线程在自旋,占用资源的线程就无法放弃资源了。 可以调用SetCriticalSectionSpinCount来修改关键段的自旋计数 ~~~c++ DWORD SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount); ~~~ ### Slim读/写锁 SRWLock结构作用和关键段相同,对一个资源进行保护不让其他线程访问它。但和关键段不同的是,SRWLock结构允许区分那些想要读取资源的值的线程(读取)和想要更新资源值的线程(写入) 首先分配一个SRWLock结构并用InitializeSRWLock初始化 ~~~c++ VOID InitializeSRWLock(PSRWLOCK SRWLock); ~~~ 初始化完成,写入者线程可以调用AcquireSRWLockExclusive将SRWLOCK对象的地址作为参数传入,以尝试获得对被保护的资源的独占访问权。 ~~~c++ VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock); ~~~ 完成资源更新后,应调用ReleaseSRWLockExclusive将SRWLock对象的地址作为参数传入,解除对资源的锁定 ~~~c++ VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock); ~~~ 对读取者线程来说换用AcquireSRWLockShared和ReleaseSRWLockShared 和关键段相比,SRWLock缺少下面两个特性: * 不存在TryEnter(Shared/Exclusive)SRWLock之类的函数:如果锁已被占用,调用AcquireSRWLock(Shared/Exclusive)会阻塞调用线程 * 不能递归地获得SRWLOCK:一个线程不能为了多次写入资源而多次锁定资源 ### 条件变量 条件变量:希望线程以原子方式释放一个资源上的锁并将自己阻塞,知道某个条件成立为止,可以使用SleepConditionVariableCS或SleepConditionVariableSRW函数 ~~~c++ BOOL SleepConditionVariableCS(PCONDITION_VARIABLE pConditionVariable, PCRITICAL_SECTION pCriticalSection, DWORD dwMilliseconds); BOOL SleepConditionVaribaleSRW(PCONDITION_VARIABLE pConditionVariable, PSRWLOCK pSRWLock, DWORD dwMilliseconds, ULONG Flags); ~~~ pConditionVariable指向一个已初始化的条件变量的指针,调用线程正在等待该变量。第二个参数是一个指向关键段或者SRWLock的指针,它用于同步对共享资源的访问。dwMilliseconds指出希望线程花多少时间来等待条件变量被触发。SleepConditionVaribaleSRW的Flags参数用来指定一旦条件变量被触发,希望线程以何种方式来获得锁:对于写入者线程应传入0,表示希望独占对资源的访问;对于读取者线程应传入CONDITION_VARIABLE_LOCKMODE_SHARED,表示希望共享对资源的访问。在指定的时间用完时,如果条件变量尚未被触发,函数返回FALSE;否则返回TRUE 当另一个线程检测到相应的条件已经满足时,比如存在一个元素让读取者线程读取,或者有足够空间让写入者线程插入新的元素,它会调用WakeConditionVariable或WakeAllConditionVariable,这样阻塞在Sleep\*函数中的线程会被唤醒 ~~~c++ VOID WakeConditionVariable(PCONDITION_VARIABLE ConditionVariable); VOID WakeAllConditionVariable(PCONDITION_VARIABLE ConditionVariable); ~~~ * 调用WakeConditionVariable会使一个在SleepConditionVariable\*函数中等待同一个条件变量被触发的线程得到锁并返回;当这个线程释放同一个锁时,不会唤醒其他正在等待同一个条件变量的线程 * 调用WakeAllConditionVariable会使一个或多个在SleepConditionVariable\*函数中等待这个条件变量被触发的线程得到资源的访问权并返回,主要针对的是读取者 最后修改:2025 年 12 月 22 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏