События PowerShell, полученные вне последовательности - PullRequest
3 голосов
/ 20 апреля 2020

Звучит как разумное ожидание, что события, запущенные из одного и того же потока, должны приниматься в том порядке, в котором они были запущены. Тем не менее, это не так. Является ли это известным / документированным поведением, и есть ли способы его исправить?

Ниже приведены два готовых к запуску фрагмента кода, которые обнаруживают проблему, протестированных с PS v5.1 под Win7 и Win10.

(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)
}

Случаи сбоев напоминают пример, размещенный в подпункте (а) выше, за исключением в нескольких пробежках также были пропущены события. Это, однако, более вероятно связано с другим вопросом События объекта на основе действий иногда теряются .

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


[ EDIT ] Я выполнил несколько дополнительные тесты для случая (b), в частности, и подтвердили, что:
  • Получающее действие (где $global:recvd++) всегда вызывается в одном и том же управляемом потоке (это было подтверждено путем сохранения и сравнения [System.Threading.Thread]::CurrentThread.ManagedThreadId между вызовами);

  • Действие получения не вводится повторно во время выполнения (это было подтверждено добавлением глобального счетчика " nesting ", переноса Действие между вызовами [System.Threading.Interlocked]::Increment/Decrement и проверкой того, что счетчик никогда не принимает никаких значений, кроме 0 и 1).

Они устраняют пару возможных условий гонки, но все же не объясняйте, почему происходит наблюдаемое поведение или как его исправить, поэтому первоначальный вопрос остается открытым.

1 Ответ

2 голосов
/ 20 апреля 2020

Это известное / документированное поведение?

"Обычно" обработка событий асинхронная по проекту . И это относится к PowerShell с такими командлетами, как Register-EngineEvent -Action. Это действительно известное и намеченное поведение. Вы можете узнать больше о событиях PowerShell здесь и здесь . Оба источника Microsoft указывают, что этот способ обработки событий является асинхронным:

PowerShell Eventing позволяет вам отвечать на асинхронные уведомления, поддерживаемые многими объектами.

и

ПРИМЕЧАНИЕ Эти командлеты могут использоваться только для асинхронных. NET событий. Невозможно настроить обработчики событий для синхронных событий с помощью командлетов событий PowerShell. Это связано с тем, что все синхронные события выполняются в одном потоке, а командлеты ожидают (требуют), чтобы события происходили в другом потоке. Без второго потока механизм PowerShell просто заблокирует основной поток, и ничего не будет выполнено.

Так что это в основном то, что вы делаете. Вы перенаправляете события из фонового задания подписчику событий, у которого определено действие, и выполняете это действие, не блокируя фоновое задание. Насколько я могу судить, ожидать больше нечего. Не указано требование для обработки перенаправленных событий в каком-либо особом порядке. Даже переключатель -Forward не обеспечивает ничего другого, кроме передачи событий:

Указывает, что командлет отправляет события для этой подписки в сеанс на локальном компьютере. Используйте этот параметр при регистрации событий на удаленном компьютере или в удаленном сеансе.

Трудно и, возможно, невозможно найти какую-либо документацию по внутренним компонентам командлетов. Имейте в виду, что Microsoft не публикует sh какой-либо документации о внутренних ресурсах из прошлого, вместо этого сами MVP должны угадывать, что происходит внутри, и писать книги об этом (резко выражены).

Так что нет необходимости обрабатывать события в определенном порядке, и PowerShell просто выполняет задачу по выполнению действий в очереди событий, ему также разрешено выполнять эти действия параллельно, чтобы ускорить обработку очереди событий.

Проверьте свои сценарии на виртуальной машине только с одним виртуальным ЦП. Неправильный порядок все еще будет происходить иногда, но гораздо реже. Поэтому меньше (реальный) параллелизм, меньше возможностей зашифровать порядок. Конечно, вы не можете предотвратить логический параллелизм, реализуемый разными потоками, выполняемыми на одном физическом ядре. Таким образом, остаются некоторые «ошибки».

Можно ли исправить это?

Я поставил «нормально» в кавычки, потому что есть способы реализовать это синхронно , Вам нужно будет реализовать свой собственный обработчик событий типа System.EventHandler. Я рекомендую прочитать эту статью , чтобы получить пример для реализации.

Другой обходной путь - хранить события в собственной очереди событий и сортировать их после сбора (запускается в 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++
}

Архивные ссылки:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...