WaitForSingleObject_调用wait方法时,线程会放弃对象锁

WaitForSingleObject_调用wait方法时,线程会放弃对象锁摘要在MicrosoftWindows平台上有几种以原子方式锁定代码和数据的不同方法。此白皮书的主要目的是向开发人员简要介绍Windows中进行锁定的不同方法以及与这些锁定有关的相应性能开销。因为未来架构将是多核架构,因此此信息非常适用。简介多线程软件应用对于提升英特尔内核架构的性能至关重要。锁定代码通常是多线程应用中运行最频繁的代码。确定要使用的锁定方法与确定应用中并行处理

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

摘要

在 Microsoft Windows 平台上有几种以原子方式锁定代码和数据的不同方法。此白皮书的主要目的是向开发人员简要介绍 Windows 中进行锁定的不同方法以及与这些锁定有关的相应性能开销。因为未来架构将是多核架构,因此此信息非常适用。

简介

多线程软件应用对于提升英特尔内核架构的性能至关重要。锁定代码通常是多线程应用中运行最频繁的代码。确定要使用的锁定方法与确定应用中并行处理一样重要。此白皮书的主要目的是向开发人员简要介绍 Windows 中进行锁定的不同方法以及与这些锁定有关的相应性能开销。Window 的某些锁定 API 可能会跳转至操作系统内核。此白皮书将详细说明跳转至内核的 API 以及相应的加入条件。

使用两种不同的锁定内核说明表示不同粒度的锁定的影响。第一种锁定内核模拟锁定和取消锁定动态链接列表(通常用于通过内存管理器维护一个空闲列表)的场景。对于多线程应用,首先需要锁定此列表,线程才能尝试分配或释放内存。第二种锁定内核表示更加精细地锁定,因为它仅获取已获得锁定的线程的 ThreadID,更新全局变量,然后释放锁定。通过采用 1 至 64 的线程数,对高、低争用场景下不同锁定的性能进行测试。每一线程获取执行 10,000,000 次某一运算的锁定,然后释放此锁定。对于这些实验,Windows XP 操作系统计时从 10 毫秒更改为 1 毫秒。WaitForSingleObject 和 EnterCriticalSectionMicrosoft Windows 平台中两种最常用的锁定方法为 WaitForSingleObject 和 EnterCriticalSection。WaitForSingleObject 是一个过载 Microsoft API,可用于检查和修改许多不同对象(如事件、作业、互斥体、进程、信号、线程或计时器)的状态。WaitForSingleObject 的一个不足之处是它会始终获取内核的锁定,因此无论是否获得锁定,它都会进入特权模式 (环路 0)。此 API 还进入 Windows 内核,即使指定的超时为 0,亦如此。此锁定方法的另一不足之处在于,它一次只能处理 64 个尝试对某个对象进行锁定的线程。WaitForSingleObject 的优点是它可以全局进行处理,这使得此 API 能够用于进程间的同步。它还具有为操作系统提供锁定对象信息的优势,从而可以实现公平性及优先级倒置。

通过对关键代码段实施 EnterCriticalSection 和 LeaveCriticalSection API 调用,可以使用EnterCriticalSection。此 API 具有 WaitForSingleObject 所不具备的优点,因为只有存在锁定争用时,才会进入内核。如果不存在锁定争用,则此 API 会获取用户空间锁定,并且在未进入特权模式的情况下返回。如果存在争用,则此 API 在内核中所采用的路径将与 WaitForSingleObject 极其相似。在低争用的情况下,由于 EnterCriticalSection 不进入内核,因此锁定开销非常低。

不足之处是 EnterCriticalSection 无法进行全局处理,因此无法为线程获取锁定的顺序提供任何保证。EnterCriticalSection 是一种阻塞调用,意味着只有线程获得对此关键区段的访问权限时,该调用才会返回。Windows 引入了 TryEnterCriticalSection,TryEnterCriticalSection 是一种非阻塞调用,无论获得锁定与否都会立即返回。此外,EnterCriticalSection 还允许开发人员使用自旋计数对关键区段进行初始化,在回退前线程会按此自旋计数尝试获取锁定。通过使用 API InitializeCriticalSectionAndSpinCount,完成初始化。自旋计数可以在此调用中进行设置,也可以在注册表中进行设置,以根据不同操作系统及其相应的线程量程对自旋进行更改。

如果存在锁定争用,则 EnterCriticalSection 和 WaitForSingleObject 都会进入内核。如果实现程度过高,从用户模式到特权模式的转换开销将会非常大。

EnterCriticalSection 和 WaitForSingleObject API 调用在对使用数千个周期的运算进行锁定时,通常不会影响性能。在这些情况下,锁定调用本身的开销不会如此突出。会导致性能降低的情况是粒度锁定,获得和释放此锁定要花费数百个周期。在这些情况下,使用用户级别锁定则非常有益。

为了说明在低争用的情况下 WaitForSingleObject 调用与 EnterCriticalSection 调用的开销情况,我们分别在 1 个和 2 个线程上运行了内存管理锁定内核。在低争用的情况下,存在加速比 (WaitForSingleObject_Time / EnterCriticalSection_Time) 大约为 5 倍的性能之差。在 2 个线程持续争用的情况下,使用 EnterCriticalSection 和使用 WaitForSingleObject 之间的差别最小。在低争用的情况下存在性能差距的原因如下:WaitForSingleObject 在每次调用时都进入内核,而 EnterCriticalSection 只有当存在锁定争用时,才进入内核。

线程数量EnterCriticalSection 时间(毫秒)WaitForSingleObject 时间(毫秒)加速比1 个线程(无争用)178191875.22 个线程(争用)53594581561.1


图 1:显示了在具有 1 个和 2 个线程的情况下,EnterCriticalSection 和 WaitForSingleObject 所对应的内存管理内核情况。EnterCriticalSection 在 1 个线程(无争用)的情况下速度较快,因为如果获得锁定,EnterCriticalSection 不会跳转至内核(特权模式)。

下图 2 说明了 EnterCriticalSection 和 WaitForSingleObject 在高争用时的开销情况,所采用的线程数介于 1 至 64 之间。在本实验中,我们在向动态链接列表中推入值和从中弹出值的同时,锁定和取消锁定该列表。目的是模拟内存分配器空闲列表,为了分配或释放内存,需频繁锁定该列表。内核会主动根据环境切换争用锁定的线程,因此在两次实验中,CPU 平均负载都是 22%。很明显,在高争用情况下,利用 EnterCriticalSection 和 WaitForSingleObject 的开销并无太大差别。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 2:显示了在具有 1 到 64 条线程的情况下,EnterCriticalSection 和 WaitForSingleObject 所对应的内存管理内核情况。在高争用情况下,与 WaitForSingleObject 和 EnterCriticalSection 所关联的开销并无太大差别。

利用英特尔® VTune 分析器通过基于事件的采样来收集时钟滴答事件,这对于确定 EnterCriticalSection 和 WaitForSingleObject 的争用情况将有所帮助。在进行该实验之前,开发人员需确保已下载了正确的内核符号。有关如何下载内核符号的说明,可从 Microsoft 开发人员网络 MSDN 上获取。

在利用 EnterCriticalSection 时,如果内核(ntoskrnl.exe 或 ntkrnlpa.exe)中花费了时间,则表明发生了争用。不存在争用时,EnterCriticalSection 调用和 LeaveCriticalSection 调用会分别将大部分时间花费在 RtlEnterCriticalSection 函数和 RtlLeaveCriticalSection 函数的 ntdll.dll 中。NTDLL.DLL 是在处理器的环路 3(非特权模式)级别上运行的动态链接库。此 NTDLL.DLL 库包含许多某个应用使用的运行时库(RTL)代码,不应与操作系统内核相混淆。EnterCriticalSection 遇到争用时,它所采取的路径将与 WaitForSingleObject 极其相似。通过查看 hal.dll、ntdll.dll 和 ntkrnlpa.exe(或 ntoskrnl.exe)中包含的函数,便可能了解是否发生 EnterCriticalSection 高争用的情况,而无需了解这些内核函数的详细信息,如图 3 中所示。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 3: 显示了在发生 EnterCriticalSection 和 WaitForSingleObject 高争用的情况下,Windows 操作系统内核、ntdll.dll 和 hal.dll 中的热门函数情况。

WaitForSingleObject 将始终跳转至 Windows 操作系统内核,但仍可以确定是否发生了针对利用此调用的锁定的高争用。当锁定发生高争用时,WaitForSingleObject 在内核、ntdll.dll 和 hal.dll 中将分别采取不同的路径。这些路径与因线程无法获得所需的锁定而导致内核根据环境切换线程有关。特别是 KiDispatchInterrupt(操作系统内核)、ZwYieldExecution(操作系统内核)、KiDispatchInterrupt(操作系统内核)、HalRequestlpi (hal.dll) 和 HalClearSoftwareInterrupt (hal.dll) 等都是能够很好地监视内核内 WaitForSingleObject 或 EnterCriticalSection 争用的函数。


WaitForSingleObject_调用wait方法时,线程会放弃对象锁


图 4: 显示了在 WaitForSingleObject 调用的无争用和高争用情况下,Windows 操作系统内核、ntdll.dll 和 hal.dll 中的热门函数情况。

如果存在锁定争用,EnterCriticalSection 和 WaitForSingleObject 都将进入内核,对于操作粒度性较高的锁定和高争用的锁定,用户级锁定是最佳选择。用户级原子锁定用户级锁定涉及利用处理器的原子操作指令以原子方式更新内存空间。原子操作指令涉及利用指令的锁定前缀,并将目标操作数指定给某个内存地址。在目前的英特尔处理器上,使用锁定前缀以原子方式运行以下指令:ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG。在跳转至内核之前,EnterCriticalSection 会利用原子操作指令尝试获取用户空间锁定。大多数指令必须明确使用锁定前缀,但 xchg 和 cmpxchg 指令例外,如果这两个指令中包含内存地址,则暗示着锁定前缀的存在。

在英特尔 486 处理器时代,锁定前缀用于在总线上声明一个锁定,这通常会带来较大的性能损失。从英特尔高能奔腾架构开始,总线锁定已转变为缓存锁定。在大多数现代架构中,如果锁定驻留在无法缓存的内存中,或锁定超出了分割缓存线的缓存线边界,则仍会在总线上声明锁定。这两种情况都不大可能出现,因此大多数锁定前缀都将转变为开销低廉的缓存锁定。

图 3 包含一个采用程序集中的一些行编写的简单锁定,以说明利用带有锁定前缀的原子操作指令获取锁定的方法。在本例中,代码仅仅测试指向的内存空间,以尝试获取某个锁定。如果该内存空间包含一个 1,则表明另一个线程已获得了该锁定。如果内存空间为 0,则表明该锁定是可用的。原子操作指令 xchg 用于尝试与内存空间交换 1。如果在执行 xchg 指令后,eax 包含 0,则表明当前线程获得了该锁定。如果在执行原子操作指令 xchg 后,eax 包含 1,则表明另一个线程已获得了该锁定。注释:edx 注册包含锁定变量的地址// 将 1 移至 eax 注册mov eax, 1// 使用解除参考的 edx 中包含的值 xchg 1lock xchg DWORD PTR[edx],eax// 如果为零,则进行测试test eax,eax// 如果不是零,则跳过jne Target


图 5: 显示了一个简单互斥锁定的程序集。

要利用用户空间锁定(使用锁定前缀),不一定要编写程序集。通过 InterlockedExchange、InterlockedIncrement、InterlockedDecrement、InterlockedCompareExchange 和 InterlockedExchangeAdd 等“Interlocked”API,Microsoft 提供了对用于同步的最常用原子操作指令的访问。这些 API 全部驻留在 kernel32.dll 中,ring3 级别(非特权模式)的应用程序将加载该 kernel32.dll。由于名称易于混淆,许多开发人员误以为 kernel32.dll 时间为内核时间,但此 dll 完全在 ring 3 级别(用户模式)上运行。它确实是用作 API 跳转至 Windows 内核的网关。Interlocked 函数根本不可能跳转至 Windows 内核(特权模式)。

为说明利用 WaitForSingleObject 和用户级锁定等开销高昂的锁定的潜在开销,使用简单的具有回退功能的用户级自旋等待锁定运行内存管理内核。在高争用和低争用情况下,用户级自旋等待的开销要低几个数量级。因此,用户级锁定便成为频繁调用的粒度锁定的首选。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 6: 内联用户级自旋等待锁定和 WaitForSingleObject 在内存管理锁定内核上的开销。用户级原子锁定上的自旋等待循环利用用户模式锁定的一个不足之处在于,内核不能用于提供自旋等待。这意味着,尝试使用 xchg 或 cmpxchg 获取锁定的应用程序将必须转而执行其他操作(如果未获得锁定)或针对该锁定自旋。如果程序员想要以用户模式实现自旋等待循环,则利用具有初始化自旋计数的 TryEnterCriticalSection 或使用诸如英特尔线程构建模块中提供的第三方锁定库将是最佳选择。从理想的角度来看,当无法获得锁定时,使线程转而执行其他操作(而非使用任何自旋等待)始终是最佳选择。

为了更好地了解自旋等待循环的构造,针对锁定内核创建了一个循环。在自旋等待循环中,首先使用原子操作指令尝试获取锁定(通常使用具有锁定前缀的 cmpxchg 或 xchg)。如果未获得锁定,代码将针对锁定内存空间的读取进行自旋,从而尝试确定其他线程释放该锁定的时间。该读取称为“可变读取”,因为它涉及 C 编程语言中的一种可变类型。可变类型具有几条与之相关联的规则,包括它们无法在注册表中更新,以及必须利用它们的内存地址来操作。在下例中,我们首先尝试获取锁定。在本例中,获得的互斥锁定将显示 0 作为 xchg 操作的结果。

如果未获得锁定,则代码将针对可变数据类型的脏读进行自旋。然后会执行测试以确定该变量为 0 还是锁定已打开。如果锁定结果为 0,则线程将再次尝试以原子方式获取锁定,并跳转至要执行的受保护代码。if (GETLOCK(lock) != 0){While (VARIOUS_CRITERIA) {_asm pause; // 自旋循环中的暂停指令if ((volatile int*)lock) == 0) // 针对脏读(而非锁定)进行自旋 {if (GETLOCK(lock)) == 0) {goto PROTECTED_CODE; } }}BACK_OFF_LOCK(lock); // 回退锁定或执行其他操作}PROTECTED_CODE:


图 6: 用于原子锁定的有效自旋等待循环。请注意回退代码以及针对可变读取(而非锁定)的自旋。通过自旋等待循环回退锁定由于以下几个原因,利用用户空间自旋等待循环是很危险的。操作系统无法了解到处理器在自旋循环中并未执行有用的操作。因此它会允许线程继续自旋,直至用尽其量程。量程是指分配给处理器上运行的每个线程的时间片。此问题在线程量程较高的 Windows 服务器平台上显得尤为严重。这不仅会大大增加 CPU 的使用率和处理器浪费的能源,而且还会给应用程序的性能带来负面影响,这是因为占用锁定的线程无法获取处理器以释放锁定。在设计自旋等待循环时,使每个线程具有回退锁定的功能是很重要的。这确保了不会有太多的线程同时主动获取同一个锁定。

回退某个锁定可通过不同的方式实现。最简单的方法是获取计数锁定,使用该方法,应用程序可以在尝试获取某一锁定达到特定次数后,通过请求环境切换回退该锁定。该尝试计数易于调整,此方法也确实常常根据硬件线程数的不同而有所调整。自旋的挂钟时间也将随处理器频率发生变化,这使得此代码难于维护。锁定的指数回退首先将不断尝试获取锁定,但会将较长时间的等待视作线程不大可能获得此锁定。指数回退自旋等待具有更强的可伸缩性。在 Windows 网络 [1] 中通常使用第二种方法。

图 7 显示了某个内联程序集 xchg 锁定在其自旋等待循环中具有回退功能和不具有回退功能时的效果。请注意具有回退功能的锁定的性能远远高于不具有回退功能的锁定的性能。这通常是因偶尔出现以下情况所致:某个占用锁定的线程根据环境被切换出去。该线程必须等待,直至获取处理器后才能释放锁定。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 7: ThreadID 锁定内核上具有回退功能的锁定和不具有回退功能的锁定。针对可变读取的自旋和针对锁定尝试的自旋开发人员在开发其自己的自旋等待循环时常犯的一个错误是试图针对原子操作指令(而非可变读取)进行自旋。针对脏读进行自旋(而不是尝试获取锁定)会消耗较少的时间和资源。这使得应用程序仅在锁定可用时才尝试获取锁定。

图 8 显示了对于 xchg 而言,针对锁定自旋和针对可变读取自旋所用时间的差异。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 8: threadID 锁定内核上具有回退功能的锁定和不具有回退功能的锁定。

一个常见的误区是利用 cmpxchg 指令的锁定的开销低于利用 xchg 指令的锁定的开销。由于首先运行 cmp,cmpxchg 不会尝试以独占模式获取锁定,因此造成了此误解。图 9 显示了 cmpxchg 指令与 xchg 指定的开销并无太大差别。

WaitForSingleObject_调用wait方法时,线程会放弃对象锁

图 9: 在 ThreadID 锁定内核上比较 xchg 和 cmpxchg。结论针对各种不同的情况利用适当的锁定对于确保 Microsoft Windows 环境中的性能和可伸缩性至关重要。即使不存在锁定争用的情况下,利用 WaitForSingleObject 对于 Windows 内核而言,仍是开销高昂的调用。如果将会频繁调用该锁定,并且只存在少量针对该锁定的争用,则应避免此类型的锁定。因此,对于粒度锁定,WaitForSingleObject 可能很危险。EnterCriticalSection 会首先尝试获取用户级锁定,如果存在锁定争用,则会跳转至内核。为帮助开发人员省去编写自己的自旋等待循环的麻烦,Microsoft 添加了 TryEnterCriticalSection 调用,该调用在跳转至内核之前会尝试获取某个自旋中的用户级锁定。

为了了解用户级自旋等待循环,我们在此白皮书中提供了基本的构造和性能建议。回退功能和针对可变读取的自旋对于自旋等待性能至关重要。参考信息[1]
http://www.microsoft.com/technet/itsolutions/network/deploy/depovg/tcpip2k.mspx*

[2] Chynoweth Michael针对英特尔® EM64T 或 32 位英特尔® 架构实现可伸缩的原子锁定。from
http://freeeim01.blog.163.com/blog/static/162339664201031944135253/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/191369.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Java中的三种注释类型「建议收藏」

    Java中的三种注释类型「建议收藏」注释:用于说明解释程序的文字就是注释。Java中的注释有三种:单行注释多行注释文档注释(Java特有)注释的作用有什么?主要就是提高了代码的阅读性,是调试程序的重要方法。当然,写注释也是一种良好编程习惯。可以将自己的思想通过注释先整理出来,再用代码去体现。来看看具体的使用吧!单行注释格式://注释文字多行注释格式:/*注释文字*/下面给出单行注释和多行注释的示例://单行注释publicclassHelloWorld{/* 程序入口

  • Android开发 屏幕适配之像素密度适配

    Android开发 屏幕适配之像素密度适配由于市场上采用Android系统的设备种类繁多,迫使Andriod开发人员不得不做烦人的适配工作。适配工作包括对安装不同Android版本的设备进行适配,对不同屏幕的设备进行适配等。而屏幕适配又包括:屏幕尺寸(small,normal,large,xlarge,这些在Android3.2以上版本开始不建议使用,转而使用最小屏幕宽度如sw600dp,最小宽度,最小高度等)屏幕

  • JAVA byte int 0xff 0xffffffff

    JAVA byte int 0xff 0xffffffffbyteb=0xff;这样无法通过编译。因为这时的0xff,是作为int类型的,其值为255,二进制记作0000000000000000 0000000011111111,另外,JAVA这里的二进制是用补码的。而byte的范围是-127~128,所以编译器无法通过。如果要想通过编译,应该如下:byteb=(byte)0xff;这时0xff,…

  • EM算法(Expectation Maximization Algorithm)详解

    EM算法(Expectation Maximization Algorithm)详解EM算法(ExpectationMaximizationAlgorithm)详解主要内容EM算法简介预备知识极大似然估计Jensen不等式EM算法详解问题描述EM算法推导EM算法流程EM算法优缺点以及应用1、EM算法简介  EM算法是一种迭代优化策略,由于它的计算方法中每一次迭代都分两步,其中一个为期望步(E步),另一个为极大步(M步),所以算法被称…

  • k8s pod控制器_k8s多人用吗

    k8s pod控制器_k8s多人用吗k8sPod控制器的介绍ReplicaSet(RS)Deployment(Deploy)扩缩容镜像更新版本回退金丝雀发布Horizontal Pod Autoscaler(HPA)DaemonSet(DS)JobCronJob(CJ)StatefulSet(有状态)StatefulSet的金丝雀发布k8s的Pod控制器详解主要介绍各种Pod控制器的详细使用。Pod控制器的介绍在kubernetes中,按照Pod的创建方式可以将其分为两类:自主式Pod:kubernetes直接创建出来的Pod,这

  • 通达信5分钟.lc5和.lc1文件格式

    通达信5分钟.lc5和.lc1文件格式一、通达信日线*.day文件文件名即股票代码每32个字节为一天数据每4个字节为一个字段,每个字段内低字节在前00~03字节:年月日,整型04~07字节:开盘价*100,整型08~11字节:最高价*100,整型12~15字节:最低价*100,整型16~19字节:收盘价*100,整型2…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号