SetEvent ResetEvent WaitForMultipleObjectsEx - Race condition?

时间:2016-08-20 19:26:26

标签: multithreading winapi

我无法理解PulseEvent或竞争条件。但要避免这种情况,我会尝试SetEvent而不是ResetEvent每次WaitForMultipleObjectsEx之前。

这是我的流程:

  • 线程一 - 使用CreateEvent创建自动重置事件,然后我生成并告诉线程TWO。
  • 线程一 - 告诉线程TWO运行。
    • 线程TWO将对事件执行ResetEvent,然后立即启动WaitForMultipleObjectsEx事件以及其他一些用于文件监视的内容。如果WaitForMultipleObjectsEx返回,并且不是由于该事件,则立即重新启动循环。如果WaitForMultipleObjectsEx返回,由于事件将发出信号,则不要重新启动循环。

现在想象一下这个案子:

  • 线程TWO - 循环正在运行
  • 线程一 - 需要添加路径,因此它会(1)SetEvent,然后(2)向线程2发送另一条消息以添加路径,然后(3)向线程2发送消息重启循环。
    • 添加路径和重启循环的消息不会进入线程TWO,除非我在TWO中停止循环,这由SetEvent完成。线程TWO将看到它由于事件而停止,因此它不会重新启动循环。所以现在它将获取添加路径的消息,因此它将添加路径,然后重新启动循环。
  • 线程一 - 需要停止线程,因此它会执行(1)SetEvent然后(2)等待消息线程2,当它获得该消息时它将终止该线程。

这会避免竞争条件吗?

谢谢

2 个答案:

答案 0 :(得分:4)

假设循环需要连续两次中断。你在线程ONE和线程TWO上想象一系列类似的事件:

  1. Thread ONE意识到第一次中断已经完成。
  2. 线程ONE发送一条消息告诉TWO重启等待循环。
  3. 线程TWO读取消息"重新启动等待循环"。
  4. 线程TWO重置事件。
  5. 线程TWO开始等待。
  6. 线程ONE现在意识到需要再次中断。
  7. 线程ONE设置事件以要求再次中断。
  8. 线程ONE发送与第二次中断相关的消息。
  9. 线程TWO停止循环,接收有关第二次中断的消息。
  10. 但是既然你无法控制两个线程之间的时间,那么它可能会发生这样的事情:

    1. Thread ONE意识到第一次中断已经完成。
    2. 线程ONE发送一条消息告诉TWO重启等待循环。
    3. 线程ONE现在意识到需要再次中断。
    4. 线程ONE设置事件以要求再次中断。
    5. 线程TWO读取消息"重新启动等待循环"。
    6. 线程TWO重置事件。
    7. 线程TWO开始等待。
    8. 线程ONE发送有关第二次中断的消息,但是TWO没有收听!
    9. 即使消息传递机制是同步的,所以ONE不会继续直到TWO读取消息,它可能会这样:

      1. Thread ONE意识到第一次中断已经完成。
      2. 线程ONE发送一条消息告诉TWO重启等待循环。
      3. 线程TWO读取消息"重新启动等待循环",然后换出。
      4. 线程ONE现在意识到需要再次中断。
      5. 线程ONE设置事件以要求再次中断。
      6. 线程TWO重置事件。
      7. 线程TWO开始等待。
      8. 线程ONE发送有关第二次中断的消息,但是TWO没有收听!
      9. (显然,如果你使用PulseEvent会发生类似的事情。)

        一个快速的解决方案是使用TWO的第二个事件在适当的点发信号,即在重置主事件之后但在等待它之前,但这看起来有点不优雅并且也没有非常概括好。如果你可以保证在接近足够的连续时间内永远不会有两次中断,你可能只是选择忽略竞争条件,但请注意,很难对此进行推理,因为对于它可能需要多长时间没有理论上的限制。线程TWO在换出后恢复运行。

        各种替代方案取决于消息如何在线程之间传递以及任何其他约束。 [如果您可以提供有关当前实施的更多信息,我会相应地更新我的答案。]

        这是一些更明显选项的概述。

        如果消息传递机制是同步的(如果线程ONE在继续之前等待线程TWO接收消息),则使用单个自动重置事件应该正常工作。线程ONE不会设置事件,直到线程TWO收到重启循环消息。如果在线程TWO开始等待时已经设置了事件,那只意味着紧接着有两次中断;两个人永远不会停止等待即将到来的消息。 [这个潜在的失速是我能想到为什么你可能不想使用自动重置事件的唯一原因。如果您有其他问题,请编辑您的问题以提供更多详细信息。]

        如果发送邮件是非阻止的,并且您还没有锁定特定解决方案,那么这些选项中的任何一个都可能是明智的:

        • 用户模式APC(QueueUserAPC函数)提供消息传递机制,可自动中断可警告的等待。

        • 您可以实现一个简单队列(受关键部分保护),该队列使用事件来指示是否有待处理的消息。在这种情况下,您可以安全地使用手动重置事件,前提是只有当您持有保护队列的相同关键部分时才能对其进行操作。

        • 您可以将自动重置事件与任何类型的线程安全队列结合使用,前提是该队列只允许您在不阻塞的情况下测试空白。这里的想法是线程ONE总是在设置事件之前将消息插入队列,如果线程TWO看到事件已设置但事实证明队列为空,则事件是忽略。如果要考虑效率,您甚至可以找到合适的无锁队列实现。 (我不建议你自己尝试。)

        (所有这些机制也可以通过使用第二个事件对象来实现。)

        我不建议采用以下方法,但如果你碰巧已经使用其中一种方法进行消息传递,那么就是如何让它发挥作用:

        • 如果您使用命名管道进行消息传递,则可以在线程TWO中使用异步I / O.线程TWO将在内部使用自动重置事件,您在发出I / O调用时指定事件句柄,Windows在I / O到达时设置它。从线程ONE的角度来看,只有一个操作。从线程TWO的角度来看,如果设置了事件,则肯定有消息可用。 (我相信这有点类似于your original approach,你只需要提前发出I / O电话而不是之后。)

        • 如果您正在使用窗口队列进行消息传递,则MsgWaitForMultipleObjectsEx()函数允许您同时等待窗口消息和其他事件。

        PS:

        文档中提到的PulseEvent的其他问题是,这可能发生:

        1. 线程TWO开始等待。
        2. 线程TWO被Windows抢占,线程上的所有用户代码都停止运行。
        3. 线程ONE使事件发生。
        4. Windows重新启动线程TWO,等待恢复。
        5. 线程ONE发送一条消息,但是TWO没有收听。
        6. (就个人而言,我有点失望的是内核没有处理这种情况;我原以为它可能设置一个标志,表示等待不应该是但是我只能假设这是不切实际的原因。)

答案 1 :(得分:1)

自动重置事件

请尝试更改流程,以便只有SetEvent和WaitForMultipleObjectsEx以及自动重置事件。如果需要,您可以创建更多事件,例如每个线程都有自己的事件:一个用于获取通知,另一个用于报告其状态更改 - 您可以定义最适合您需求的方案。

由于会有自动重置事件,因此既没有ResetEvent也没有PulseEvent。

如果您能够以这种方式改变算法流程的逻辑 - 程序将变得清晰,简单和可靠。

我建议这是因为这是我们的应用程序自Windows NT 3.51时代以来的工作方式 - 我们只使用SetEvent和WaitForMultipleObjects(没有Ex后缀)设法完成所需的一切。

关于PulseEvent,正如你所知,它是非常不可靠的,尽管它存在于Windows NT的第一个版本 - 3.1 - 它可能是可靠的,但现在不是。

要创建自动重置事件,请使用CreateEvent API函数的bManualReset参数(如果此参数为TRUE,则该函数会创建手动重置事件对象,这需要使用ResetEvent函数来设置事件状态无信号 - 这不是你需要的)。如果此参数为FALSE,则该函数会创建一个自动重置事件对象,并且系统将在释放单个等待线程后自动将事件状态重置为无信号,即在WaitForMultipleObjects或WaitForSingleObject或其他明确等待的等待函数之后为此事件发出信号。

这些自动重置事件非常可靠且易于使用。

关于PulseEvent - 即使微软已经承认PulseEvent不可靠且不应该使用 - 请参阅https://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx - 因为只有那些线程会被通知在"等待&#34 ;现在调用PulseEvent的状态。如果它们处于任何其他状态,它们将不会被通知,并且您可能永远不会确定线程状态是什么,并且,即使您负责程序流,也可以通过操作系统更改状态。你的程序逻辑。等待同步对象的线程可以通过内核模式异步过程调用(APC)暂时从等待状态中移除,然后在APC完成后返回到等待状态。如果在线程从等待状态中删除期间发生对PulseEvent的调用,则不会释放该线程,因为PulseEvent仅释放那些在被调用时正在等待的线程。

您可以在以下链接中找到有关内核模式APC的更多信息:

手动重置事件

手动重置事件并没有那么糟糕。 :-)当您需要通知仅发生一次的全局状态更改的多个实例时,您可以可靠地使用它们,例如应用程序退出。自动重置事件只能用于通知一个线程(因为如果更多的踏板同时等待自动重置事件并且您设置了事件,则会存在一个随机线程并将重置事件,但剩余的行为同样等待事件的线程将是未定义的。从Microsoft文档中我们可以假设只有一个线程会退出,而其他线程肯定不会退出,但这在文档中并没有明确说明。无论如何,我们必须考虑以下引用:“不要假设先进先出(FIFO)订单。内核模式APC等外部事件可以更改等待顺序“Source - https://msdn.microsoft.com/en-us/library/windows/desktop/ms682655(v=vs.85).aspx

因此,当您需要快速通知所有线程时 - 只需将手动重置事件设置为信号状态,而不是发出每个线程的每个自动重置事件的信号。一旦发出手动重置事件的信号,就不要再调用ResetEvent了。这个解决方案的缺点是线程需要在WaitForMultipleObjects数组中传递一个额外的事件句柄。数组大小是有限的,尽管MAXIMUM_WAIT_OBJECTS为64,实际上我们从未达到接近此限制。

您可以从https://www.codeproject.com/Articles/39040/Auto-and-Manual-Reset-Events-Revisited

获得有关自动重置事件和手动重置事件的更多提示