【问题标题】:PowerShell events received out of sequence收到乱序的 PowerShell 事件
【发布时间】:2020-05-04 07:19:20
【问题描述】:

从同一个线程触发的事件应该按照触发顺序被接收,这听起来像是一个合理的期望。然而,情况似乎并非如此。这是一种已知/记录在案的行为,是否有任何补救措施来纠正它?

以下是两个可运行的代码 sn-ps,它们在 Win7 和 Win10 下使用 PS v5.1 进行了测试。

(a) 从单独作业(即不同进程)中的线程触发的事件。

$events = 1000
$recvd = 0
$ooseq = 0

$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
    $global:recvd++ 
    if($global:recvd -ne $event.messageData) {
        $global:ooseq++
        ("-?- event #{0} received as #{1}" -f $event.messageData, $global:recvd)
} }

$run = Start-Job -ScriptBlock {
    Register-EngineEvent -SourceIdentifier 'Posted' -Forward
    for($n = 1; $n -le $using:events; $n++) {
        [void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
} }
Receive-Job -Job $run -Wait -AutoRemoveJob

Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob

if($events -eq $script:recvd) {
    ("total {0} events" -f $events)
} else {
    ("total {0} events events, {1} received" -f $events, $recvd)
}
if($ooseq -ne 0) {
    ("{0} out-of-sequence events" -f $ooseq)
}

来自失败案例的样本输出(在一批 100 次连续运行中)。

   -?- event #545 received as #543
   -?- event #543 received as #544
   -?- event #546 received as #545
   -?- event #544 received as #546
   total 1000 events
   4 out-of-sequence events

(b) 从单独的运行空间(即不同的线程)触发的事件。

$recvd = 0
$ooseq = 0

$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
    $global:recvd++ 
    if($recvd -ne $event.messageData) {
        $global:ooseq++
        ("-?- event #{0} received as #{1}" -f $event.messageData, $recvd)
}}

$sync = [hashTable]::Synchronized(@{})
$sync.Host = $host
$sync.events = 1000
$sync.posted = 0

$rs = [runspaceFactory]::CreateRunspace()
$rs.ApartmentState = "STA"
$rs.ThreadOptions = "ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("sync",$sync)

$ps = [powerShell]::Create().AddScript({
    for($n = 1; $n -le $sync.events; $n++) {
        $sync.Host.Runspace.Events.GenerateEvent('Posted', $null, $null, $n)
        $sync.posted++
}})
$ps.runspace = $rs
$thd = $ps.BeginInvoke()
$ret = $ps.EndInvoke($thd)
$ps.Dispose()

Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob

if($sync.events -eq $recvd) {
    ("total {0} events" -f $sync.events)
} else {
    ("total {0} events fired, {1} posted, {2} received" -f $sync.events, $sync.posted, $recvd)
}
if($ooseq -ne 0) {
    ("{0} out-of-sequence events" -f $ooseq)
}

失败案例类似于上面 (a) 下发布的示例,除了一些运行也完全丢弃了事件。然而,这更有可能与另一个问题Action-based object events sometimes lost有关。

total 1000 events fired, 1000 posted, 999 received
484 out-of-sequence events


[ EDIT ] 我专门针对案例 (b) 进行了一些额外的测试,并确认:
  • 接收动作($global:recvd++)总是在同一个托管线程上调用(这通过保存和比较调用之间的[System.Threading.Thread]::CurrentThread.ManagedThreadId得到确认);

  • 在执行期间未重新输入接收动作(这已通过添加全局“嵌套”计数器来确认,将动作包装在 [System.Threading.Interlocked]::Increment/Decrement 调用之间并检查计数器永远不会取除01 以外的任何值。

这些消除了几个可能的竞争条件,但仍然没有解释观察到的行为发生的原因或如何纠正它,因此原始问题仍然悬而未决。

【问题讨论】:

    标签: powershell events


    【解决方案1】:

    这是一种已知/记录在案的行为吗?

    “通常”事件处理是异步的设计。在 PowerShell 中使用 Register-EngineEvent -Action 之类的 cmdlet 就是这种情况。这确实是已知的和预期的行为。您可以阅读有关 PowerShell 事件 herehere 的更多信息。微软的两个消息来源都指出,这种事件处理方式是异步的:

    PowerShell 事件允许您响应异步通知 那么多对象都支持。

    注意 这些 cmdlet 只能用于异步 .NET 事件。它的 无法使用 PowerShell 事件 cmdlet。这是因为所有同步事件 在同一线程上执行,并且 cmdlet 期望(要求) 事件将发生在另一个线程上。没有第二个线程, PowerShell 引擎只会阻塞主线程,什么都不会 永远被处决。

    所以这基本上就是你正在做的事情。您将后台作业中的事件转发给定义了操作的事件订阅者,并在不阻塞后台作业的情况下执行该操作。据我所知,没有什么可期待的了。没有指定以任何特殊顺序处理转发的事件的要求。甚至-Forward 开关也不能确保更多,除了传递事件:

    表示 cmdlet 将此订阅的事件发送到 本地计算机上的会话。使用此参数时 在远程计算机或远程会话中注册事件。

    很难甚至不可能找到有关 cmdlet 内部的任何文档。请记住,Microsoft 不会发布任何关于过去的内部结构的文档,而是由 MVP 来猜测内部发生的事情并写关于它的书籍(激烈地表达)。

    因此,由于不需要按特定顺序处理事件,而 PowerShell 仅具有对事件队列执行操作的任务,因此也允许并行执行这些操作以加速事件队列的处理.

    在只有一个 vCPU 的 VM 上测试您的脚本。错误的顺序有时仍然会发生,但很少发生。所以更少的(真正的)并行性,更少的扰乱顺序的可能性。当然,您不能阻止逻辑并行性,由在一个物理内核上执行的不同线程实现。所以仍然存在一些“错误”。

    有办法改正吗?

    我将“通常”放在引号中,因为有一些方法可以同步实现它。您必须实现自己的System.EventHandler 类型的事件处理程序。我建议阅读this article 以获取实现示例。

    另一种解决方法是将事件存储在自己的事件队列中,并在收集后对它们进行排序(在 ISE 中运行,尚未在 PS 中运行):

    $events = 10000
    $recvd = 0
    $ooseq = 0
    $myEventQueue = @()
    
    $job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {$global:myEventQueue += $event}
    
    $run = Start-Job -ScriptBlock {
        Register-EngineEvent -SourceIdentifier 'Posted' -Forward
        for($n = 1; $n -le $using:events; $n++) {
            [void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
        }
    }
    Receive-Job -Job $run -Wait -AutoRemoveJob
    Unregister-Event -SourceIdentifier 'Posted'
    Receive-Job -Job $job -Wait -AutoRemoveJob
    
    Write-Host "Wrong event order in unsorted queue:"
    $i = 1
    foreach ($event in $myEventQueue) {
        if ($i -ne $event.messageData) {
            Write-Host "Event $($event.messageData) received as event $i"
        }
        $i++
    }
    
    $myEventQueue = $myEventQueue | Sort-Object -Property EventIdentifier
    
    Write-Host "Wrong event order in sorted queue:"
    $i = 1
    foreach ($event in $myEventQueue) {
        if ($i -ne $event.messageData) {
            Write-Host "Event $($event.messageData) received as event $i"
        }
        $i++
    }
    

    存档链接:

    【讨论】:

    • 感谢您的指点。然而,这些(和其他)是指事件从多个不同线程触发和/或在多个不同线程中接收的情况。但是发布的案例的所有事件都从一个和 same 线程触发,并且也在(另一个)一个和 same 线程中接收。从表面上看,这似乎属于引用 elsewhere 在 SO“在单个执行线程内的规则......线程生成的副作用和异常按指定的顺序可见 ”。原来这不适用于这里,但为什么呢?
    • 我仍然认为是这样,因为事件处理不必是同步的。生成的事件首先添加到事件队列中。然后任何处理都可以访问/处理它们。由于不需要同步性,因此允许软件/硬件优化控制流,例如并行处理事件、进行流水线操作、使用推测代码执行的结果等。这可能会导致您触发的事件的处理顺序不同。
    • 您的引文指出,您的事件将按照您指定的顺序在您的线程中触发,这肯定是这种情况。但是事件处理不是由您启动的这个线程完成的 (Start-Job)。所以处理(你定义的动作)可能确实以另一个顺序发生。
    • 我的问题的重点是“ 同一个线程触发的事件,还有收到(另一个) 一个和同一个线程”。我希望在这种特殊情况下保留订单。 FWIW 用 C# 重写的相同代码可以完美运行 - 在数千次测试运行期间,没有一个事件丢失或重新排序,每个测试运行数百万个事件。所以问题仍然存在,为什么 PS 不会或不能这样做。
    • @dxiv 我不会依赖“在(另一个)同一个线程中收到”。对于 1000 个事件的事件队列,事件订阅者可能正好触发了 1000 个其他线程(对于每个操作)。这些线程不必按任何顺序处理,因为它们之间没有连接,并且:仍然可以通过评估事件 ID 重新创建“顺序”,因此实际上不需要按任何顺序处理它们。想象一下,您的事件被并行处理,然后一个线程被中断,然后$global:recvd++ 将不再按您预期的顺序发生。
    猜你喜欢
    • 1970-01-01
    • 2011-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多