热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《WindowsviaC/C++》学习笔记——用户模式的“线程同步”之“关键代码段”

上一篇讨论有关“互锁函数家族”处理线程同步的方法。本书中还描述了“高速缓存行”(cacheline)概念,还给了一些线程同步的建议

  上一篇讨论有关“互锁函数家族”处理线程同步的方法。本书中还描述了“高速缓存行”(cache line)概念,还给了一些线程同步的建议,比如要实现单个变量的同步,应该避免使用volatile关键字,如果实在需要对单个变量进行同步,最好使用互锁函数(传递的是地址,所以每次取值都从内存取得)。感觉这些东西没有什么好讲的,看看书就可以了。

  然后,本书提供了另外一种工作在用户模式的线程同步的方法:关键代码段。虽然不能协调多个进程中的线程,但是我确实最经常使用这种机制来协调单个进程中线程的同步(因为好用^_^)。

 

  关键代码段,所谓“代码段”,也就是说“一段代码”。这段代码的执行是以原子的形式执行的,独占着某些资源,任何想要访问这些资源的其他线程在关键代码段执行完成之前只能等待。

 

  要让实现关键代码段,需要以下5个步骤:

1、初始化一个关键代码段结构。

2、一个线程进入关键代码段。

3、对资源进行原子操作。

4、该线程离开关键代码段。

5、删除关键代码段。

 

  上述5个步骤,除了第3步是程序员需要自己进行设计,其他都有相应的API函数可以被调用。

  首先看下关键代码段结构:CRITICAL_SECTION。这个数据结构是有明确文档定义的,但是微软认为里面的内容不需要我们去了解。所以该结构内部的成员对我们来说是透明的。该结构在WinBase.h文件中被定义为RTL_CRITICAL_SECTION。

 

  在第1步,你需要初始化这个结构,调用InitializeCriticalSection函数,传递一个该结构的指针。在该函数内部会设置CRITICAL_SECTION的内部成员。

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

 

  在第5步,当你不再需要这个关键代码段的时候,你需要呼叫函数DeleteCriticalSection来清除CIRTICLA_SECTION结构,同样是传递一个该结构的指针。

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

 

  当你调用InitializeCriticalSection函数初始化了一个关键代码段之后,你就可以让你的线程通过这个关键代码段来对某写资源进行原子访问。

  在第2步,你需要进入一个关键代码段,呼叫函数EnterCriticalSection,同样传递一个已经初始化了的CRITICAL_SECTION结构指针

VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

 

  当调用该函数的时候,会发生以下三种的处理:

  如果没有其他线程在访问相关资源,那么该函数更新内部数据,指明当前线程已经被赋予访问权并立即返回,使得该线程可以继续执行。

  如果CRITICAL_SECTION结构的成员变量指明了当前线程已经被赋予了访问权,则更新内部数据,指明该线程被赋予了多少次访问权(递增计数)。这种情况比较少见,只有在一个线程内部多次调用EnterCriticalSection函数才会发生。

  如果CRITICAL_SECTION结构的成员变量指明了当前已经有一个线程被赋予了访问权,那么该函数将当前线程设置为等待状态。然后更新内部数据,一旦正在访问资源的线程离开的关键代码段(调用LeaveCritlcalSection,后面会讲),该线程就会处于可调度状态。  

 

  如果当前已经初始化了一个关键代码段cs,同时存在着2个线程:T1和T2。然后T1呼叫EnterCriticalSection(&cs)函数进入该关键代码段,然后T2也呼叫EnterCriticalSection(&cs),那么T2会进入等待状态,等待T1离开cs所代表的关键代码段后,T2才恢复到可调度状态。

  实际上,等待的线程可能会超时,然后抛出一个异常。该时间数值由注册表中的一个值表示的:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager

  该值默认为2592000s,即大约3 0天。

 

  从内部来讲,EnterCriticalSection函数并不复杂,在内部使用互锁函数,执行的只是一些简单的测试。但是这些测试都是以原子的方式进行的。

 

  你可以使用函数TryEnterCriticalSection来代替EnterCriticalSection。

BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);

 

  该函数不会让呼叫的线程进入等待状态,相反,该函数返回一个BOOL变量,指明当前线程能否进入关键代码段。

  该函数可以让线程快速地查看能否获取某些资源,如果不能,该线程可以继续做其他的事情。如果该函数返回TRUE,说明CRITICAL_SECTION的成员变量已经更新,可以进入对应的关键代码段了。

  如果一个线程顺利地进入了关键代码段,那么意味着它可以独占某些资源,此时可以以特定的算法来对某些资源访问和操作了(对应步骤3)。

 

  第4步,在一个线程对某些资源访问或操作完成之后,必须离开关键代码段,调用函数LeaveCriticalSection函数,同样传递一个CRITICAL_SECTION结构的指针。

VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

 

  调用该函数的时候,CRITICAL_SECTION的内部数据会被更新。该函数使计数递减1,指明当前线程被赋予的访问权次数。

  如果递减后,该计数仍然大于0,则该函数什么也不做,只是简单的返回而已。

  递减后,如果该计数等于0,它就更新成员变量,并查看那些因为调用EnterCriticalSection而处于等待状态的线程,如果存在这样的线程,它就更新成员变量,并选择其中一个,让其变成可调度状态;如果没有线程在等待,则更新成员变量,说明此时没有线程在访问资源。

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

 

 

下面讨论有关“关键代码段与循环锁”的问题。

 

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

  为了提高关键代码段的性能,微软为关键代码段提供了循环锁机制。当一个线程调用EnterCriticalSection函数的时候,可以使用循环锁进行循环查询,这样就可以多次尝试访问资源。只有当每次尝试均告失败之后,该线程才转入内核模式。

  如此一来,只要在这组尝试失败以前,原先占有资源的线程离开了关键代码段,那么该尝试访问资源的线程便可尝试成功,这样避免了转入内核模式的执行,提高的性能。

  要将循环锁用于关键代码段,必须将一个关键代码段与一个循环次数关联起来,可以调用InitializeCriticalSectionAndSpinCount函数,即能够初始化关键代码段,也可以将一个循环锁查询次数与之绑定。

BOOL InitializeCriticalSectionAndSpinCount(
   PCRITICAL_SECTION pcs,     
//关键代码段结构指针
   DWORD dwSpinCount);        //循环锁循环查询次数(尝试访问资源次数)

 

  要注意的情况是,如果在单CPU的计算机上,该函数的第二个参数dwSpinCount会被忽略,永远为0,因为在单CPU上,如果一个线程在循环尝试请求资源,而当前拥有资源的线程不可能被调度,资源是无法释放的。

  也可以修改一个关键代码段循环锁循环次数:

DWORD SetCriticalSectionSpinCount(
   PCRITICAL_SECTION pcs,    
//关键代码段结构指针
   DWORD dwSpinCount);       //循环锁循环次数

 

  当然,如果运行在单CPU计算机上,dwSpinCount参数会被忽略。

  一般地,经验告诉我们,设置dwSpinCount为4000,即让线程循环4000次来尝试获取资源。

 

  InitializeCriticalSection函数可能会运行失败(在资源极度贫乏的情况下),由于微软忽略了这个问题,所以它的返回类型是VOID。在这种情况下,你可以使用InitializeCriticalSectionAndSpinCount函数,它返回一个BOOL型数据,指明初始化关键代码段是否成功。

 

  当使用关键代码段的时候,可能会出现对关键代码段的争用,即当前线程调用EnterCriticalSection函数的时候,该关键代码段的访问权已经被另一个线程所拥有,此时发生了争用。此时关键代码段使用事件内核对象处理线程同步问题。

  当在内存资源极度贫乏的情况下,此时线程争用关键代码段,那么关键代码段可能无法创建必要的事件内核对象,这个时候EnterCriticalSection函数会产生一个EXCEPTION_INVALID_HANDLE异常,你可以采取一下两种方法处理之:

1、使用结构化异常的方法,当异常产生的时候,不访问关键代码段保护的资源,当内存变成可用状态的时候,再次呼叫EnterCriticalSection函数。

2、使用InitializeCriticalSectionAndSpinCount函数创建关键代码段的时候确保设置了dwSpinCount参数的高信息位,当该函数发现dwSpinCount的高信息位被设置,它会创建一个事件内核对象,并将该内核对象与关键代码段关联起来。如果事件内核对象无法创建,函数返回FALSE。如果创建成功那么就意味着EnterCriticalSection总能运行成功,因为总是先创建事件内核对象。

 

转:https://www.cnblogs.com/wz19860913/archive/2008/08/07/1262765.html



推荐阅读
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 本文详细介绍了云服务器API接口的概念和作用,以及如何使用API接口管理云上资源和开发应用程序。通过创建实例API、调整实例配置API、关闭实例API和退还实例API等功能,可以实现云服务器的创建、配置修改和销毁等操作。对于想要学习云服务器API接口的人来说,本文提供了详细的入门指南和使用方法。如果想进一步了解相关知识或阅读更多相关文章,请关注编程笔记行业资讯频道。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • Windows2003 IIS上设置301定向,实现不带www域名跳转带www域名的方法
    打开IIS,建一个网站,主机头用不带www的域名,随便指向一个目录。然后在这个网站上点右键,属性--主目录--重定向到URL如图ÿ ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 网卡工作原理及网络知识分享
    本文介绍了网卡的工作原理,包括CSMA/CD、ARP欺骗等网络知识。网卡是负责整台计算机的网络通信,没有它,计算机将成为信息孤岛。文章通过一个对话的形式,生动形象地讲述了网卡的工作原理,并介绍了集线器Hub时代的网络构成。对于想学习网络知识的读者来说,本文是一篇不错的参考资料。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
author-avatar
蓝瑟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有