热门标签 | 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



推荐阅读
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 在Windows系统中安装TensorFlow GPU版的详细指南与常见问题解决
    在Windows系统中安装TensorFlow GPU版是许多深度学习初学者面临的挑战。本文详细介绍了安装过程中的每一个步骤,并针对常见的问题提供了有效的解决方案。通过本文的指导,读者可以顺利地完成安装并避免常见的陷阱。 ... [详细]
  • PTArchiver工作原理详解与应用分析
    PTArchiver工作原理及其应用分析本文详细解析了PTArchiver的工作机制,探讨了其在数据归档和管理中的应用。PTArchiver通过高效的压缩算法和灵活的存储策略,实现了对大规模数据的高效管理和长期保存。文章还介绍了其在企业级数据备份、历史数据迁移等场景中的实际应用案例,为用户提供了实用的操作建议和技术支持。 ... [详细]
  • 深入解析Struts、Spring与Hibernate三大框架的面试要点与技巧 ... [详细]
  • OpenAI首席执行官Sam Altman展望:人工智能的未来发展方向与挑战
    OpenAI首席执行官Sam Altman展望:人工智能的未来发展方向与挑战 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 本文详细介绍了在Linux系统上编译安装MySQL 5.5源码的步骤。首先,通过Yum安装必要的依赖软件包,如GCC、GCC-C++等,确保编译环境的完备。接着,下载并解压MySQL 5.5的源码包,配置编译选项,进行编译和安装。最后,完成安装后,进行基本的配置和启动测试,确保MySQL服务正常运行。 ... [详细]
  • 本文探讨了在使用 Outlook 时遇到的一个常见问题:无法加载 SAVCORP90 插件,导致软件功能受限。该问题通常表现为在启动 Outlook 时会收到错误提示,影响用户的正常使用体验。文章详细分析了可能的原因,并提供了多种解决方法,包括检查插件兼容性、重新安装插件以及更新 Outlook 版本等。通过这些步骤,用户可以有效解决这一问题,恢复 Outlook 的正常运行。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 本文深入探讨了Hibernate框架中乐观锁和悲观锁的机制及其多态特性。乐观锁假设数据在大多数情况下不会发生冲突,因此在读取数据时不加锁,而是在更新时检查版本号以确保数据未被修改。相比之下,悲观锁则认为数据在并发环境下容易产生冲突,因此在读取数据时立即加锁,以防止其他事务访问同一数据,从而避免潜在的数据不一致问题。文章还详细分析了这两种锁机制在实际应用中的优缺点,并介绍了Hibernate中的多态特性如何与锁机制结合,以实现更高效的数据管理和并发控制。 ... [详细]
  • 通过利用代码自动生成技术,旨在减轻软件开发的复杂性,缩短项目周期,减少冗余代码的编写,从而显著提升开发效率。该方法不仅能够降低开发人员的工作强度,还能确保代码的一致性和质量。 ... [详细]
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社区 版权所有