zoukankan      html  css  js  c++  java
  • Windows Internals 笔记——用内核对象进行线程同步

    1. 在x86平台上,一个空的系统调用大概会占用200个CPU周期,造成内核对象比用户模式下的同步机制慢几个数量级的原因,是伴随调度新线程而来的刷新告诉缓存以及错过高速缓存(即未命中),可能是成百上千个CPU周期。

    2. 对线程同步来说,内核对象中的每一种要么处于触发状态,要么处于未触发状态。例如,进程(或线程)内核对象在创建的时候总是处于未触发状态。当进程终止时,操作系统会自动使进程内核对象变为触发状态,它将永远保持这种状态,再也不会变回到未触发状态。 

    1. 等待函数

    1.1 等待函数WaitForSingleObject使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。如果线程在调用一个等待函数的时候,相应的内核对象已经处于触发状态,那么线程是不会进入等待状态的。

    2. 等待成功所引起的副作用

    2.1 对于一些内核对象来说(如 AutoResetEvent),成功地调用WaitForSingleObject 或 WaitForMultipleObjects 事实上会改变对象的状态,这就是等待成功所引起的副作用。

    2.2 WaitForMultipleObjects是以原子方式工作的,当函数检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态,避免了副作用。

    2.3 如果多个线程等待同一个内核对象,系统如何决定应该唤醒哪个线程?Microsoft的官方回答是,“算法是公平的”。每次当对象被触发时,每个线程都有机会被唤醒。这意味着线程优先级将没有效果,等待时间最长的线程不一定能得到对象。实际上,Microsoft所使用的算法只不过是众所周知的“先入先出”机制。等待实际最长的线程得到对象。但是,系统内部的一些操作可能会改变这种行为,使得它变得更加不可预测。如,线程先等待一个对象,然后线程被挂起,那么系统会忘记这个线程还在等待对象。因为系统没有理由去调度一个被挂起的线程。当后来线程恢复的时候,系统会认为这个线程才刚开始等待对象。

    2.4 我们在调试一个进程的时候,如果遇到断点,那么进程中所有的线程都会被挂起。因此,调试一个进程会使这个“先入先出”算法变得极其难以预测,因为线程会被频繁地挂起和恢复。

    3. 事件内核对象

    3.1 在所有地内核对象中,事件比其他对象要简单得多。事件包含:

    • 一个使用计数(这一点和其他内核对象一样)
    • 一个用来表示事件是自动重置事件还是手动重置事件的布尔值
    • 一个用来表示事件有没有被触发的布尔值

    3.2 有两种不同类型的事件对象:

    • 手动重置事件,当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变成可调度状态。
    • 自动重置事件,当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。

    3.3 Microsoft 为自动重置事件定义了一个等待成功所引起的副作用:当线程成功等到自动重置事件对象的时候,对象会自动地重置为未触发状态。

    4. 可等待的计时器内核对象

    4.1 用户计时器会产生 WM_TIMER 消息,WM_TIMER 消息总是优先级最低的,只有当线程的消息队列中没有其他消息时候才会被处理。可等待的计时器的处理方式与其他内核对象没有任何不同,如果计时器被触发而且线程正在等待,那么系统将唤醒线程。

    5. 信号量内核对象

    5.1 信号量内核对象用来对资源进行计数。与其他所有内核对象相同,他们也包含一个使用计数,但是它们还包含另外两个32位值:

    • 最大资源计数,表示信号量可以控制的最大资源数量
    • 当前资源计数,表示信号量当前可用资源的数量

    5.2 信号量以原子方式工作,当我们向信号量请求一个资源的时候,操作系统会检查资源是否可用,并将可用资源的数量递减,整个过程不会被别的线程打断。只有当资源计数递减完成之后,系统才会允许另一个线程请求对资源的访问。

    5.3 如果等待函数发现信号量的当前资源计数为0(信号量处于未触发状态),那么系统会让调用线程进入等待函数。当另一个线程将信号量的当前资源计数递增时,系统会记得哪个(或哪些)还在等待的线程,使它们变成可调度状态(并相应地递减当前资源计数)。

    5.4 我们没有办法在不改变当前资源计数的前提下来得到它的值(ReleaseSemaphore 传参数0无效)。

    6. 互斥量内核对象

    6.1 互斥量内核对象用来确保一个线程独占对一个资源的访问。包含:

    • 一个使用计数
    • 一个线程ID,用来标识当前占用这个互斥量的是系统中的哪个线程
    • 一个递归计数,表示这个线程占用该互斥量的次数

    6.2 互斥量与关键段的行为完全相同,但是互斥量是内核对象,而关键段是用户模式下的同步对象。(除非对资源的争夺非常激烈,这种情况下等待关键段的线程将不得不进入内核模式等待)。这意味着不同进程中的线程可以访问同一个互斥量,还意味着线程可以在等待对资源的访问权时指定一个最长等待时间。

    6.3 在用来触发普通内核对象和撤销触发普通内核对象的规则中,有一条不适用于互斥量。假设线程试图等待一个未触发的互斥量对象。在这种情况下,线程通常会进入等待状态。但是,系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果线程ID一致,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。对系统中的任何其他内核对象来说,我们都找不到这种“异常”举动。每次线程成功地等待了一个互斥量,互斥量对象的递归计数都会递增。使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。

    6.4 当目前占有访问权的线程不再需要访问资源的时候,它必须调用 ReleaseMutex 函数来释放互斥量,这个函数hi将对象的递归计数减1.如果线程成功等待了互斥量对象不止一次,那么线程必须调用 ReleaseMutex 相同次数才能使对象的递归计数变成0。当递归计数变成0的时候,函数还会将线程ID设为0,这样就触发了对象。当对象被触发的时候,系统会检查有没有其他线程在等待该互斥量。如果有,那么系统会“公平地”选择一个正在等待的线程,把互斥量的所有权给它。

    6.5 如果占用互斥量的线程在释放互斥量之前终止,系统会认为互斥量被遗弃。系统会记录所有的互斥量和线程内核对象,因此它确切的知道互斥量何时被遗弃。当互斥量被遗弃的时候,系统会自动将互斥量对象的线程ID设为0,将它的递归计数设为0。然后系统会“公平地”选择一个正在等待的线程...。这一切都和从前一样,唯一的不同之处在于等待函数不再返回通常的 WAIT_OBJECT_0, 而是 WAIT_ABANDONED。

    6.6 互斥量与关键段的比较

    7. 线程同步对象速查表

    7.1 各种内核对象与线程同步有关的行为

    7.2 Interlocked 系列函数(用户模式)从来不会使线程变成不可调度状态,它们只是修改一个值并立即返回。

    8. 其他线程同步函数

    8.1 WaitForInputIdle 函数会等待由 hProcess 标识的进程,直到创建应用程序第一个窗口的线程中没有待处理的输入为止。

    8.2 MsgWaitForMultipleObjects 或 MsgWaitForMultipleObjectsEx,使线程等待需要自己处理的消息,与 WaitForMultipleObjects 函数类似。不同之处在于,不仅内核对象被触发时调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。

    8.3 Windows 内建的调试器通过调用 WaitForDebugEvent 函数来等待这些事件,当调试器调用这个函数的时候,调试器的线程会挂起。

    8.4 SignalObjectAndWait 函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象,可以节省大量的处理时间(用户模式到内核模式的切换)。

    8.5 Windows 提供了一组新的等待遍历(Wait Chain Traversal, WCT)API,这些函数可以让我们列出所有的锁,并检测进程内部,甚至是进程之间的死锁。

     

  • 相关阅读:
    P2519 [HAOI2011]problem a
    P1084 疫情控制
    P1941 飞扬的小鸟
    NOIP填坑计划
    P2831 愤怒的小鸟
    AGC 16 D
    P3960 列队
    Python3爬虫相关软件,库的安装
    软件理论基础—— 第一章命题逻辑系统L
    软件理论基础——导论
  • 原文地址:https://www.cnblogs.com/zoneofmine/p/14044128.html
Copyright © 2011-2022 走看看