Многопоточность в PowerShell: Глубокое погружение

В какой-то момент большинство людей сталкиваются с проблемой, что простой сценарий PowerShell слишком медленно решает. Это может быть сбор данных с большого числа компьютеров в вашей сети или, возможно, создание множества новых пользователей в Active Directory одновременно. Оба этих случая являются отличными примерами того, где использование дополнительной вычислительной мощности ускорило бы выполнение вашего кода. Давайте рассмотрим, как решить эту проблему с использованием многозадачности в PowerShell!

Сеанс PowerShell по умолчанию однопоточный. Он выполняет одну команду, и когда она завершается, переходит к следующей. Это удобно, поскольку делает все повторяемым и не использует много ресурсов. Но что, если выполняемые им действия не зависят друг от друга, и у вас есть ресурсы ЦП для расходования? В этом случае пришло время начать думать о многозадачности.

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

Понимание многозадачности в PowerShell

Многозадачность – это способ выполнения более чем одной команды одновременно. Где PowerShell обычно использует один поток, существует много способов использовать более одного для параллелизации вашего кода.

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

Например, что если вы хотели бы создать одного нового пользователя в Active Directory? В этом примере нет ничего для многопоточности, потому что выполняется только одна команда. Все меняется, когда вы хотите создать 1000 новых пользователей.

Без многопоточности вы запустите команду New-ADUser 1000 раз, чтобы создать всех пользователей. Возможно, потребуется три секунды, чтобы создать нового пользователя. Для создания всех 1000 пользователей потребуется немного менее часа. Вместо использования одного потока для 1000 команд вы могли бы использовать 100 потоков, каждый из которых выполняет десять команд. Теперь, вместо того, чтобы занимать около 50 минут, вы уложитесь меньше чем за минуту!

Обратите внимание, что вы не увидите идеального масштабирования. Процесс создания и уничтожения элементов в коде займет некоторое время. Используя один поток, PowerShell должен выполнить код, и все. С несколькими потоками исходный поток, используемый для выполнения вашей консоли, будет использоваться для управления другими потоками. В определенный момент исходный поток достигнет предела своих возможностей, просто контролируя все остальные потоки.

Предварительные требования для многопоточности в PowerShell

Вы собираетесь узнать, как работает многопоточность в PowerShell на практике в этой статье. Если вы хотите следовать за мной, вот несколько вещей, которые вам понадобятся, и некоторые подробности об используемой среде.

  • Windows PowerShell версии 3 и выше – Все, если не указано явно, весь код, продемонстрированный здесь, будет работать в Windows PowerShell версии 3 и выше. В примерах будет использоваться Windows PowerShell версии 5.1.
  • Резерв процессора и памяти – Вам потребуется как минимум немного дополнительного процессора и памяти для параллелизации с PowerShell. Если у вас этого нет, вы можете не заметить улучшения производительности.

Приоритет №1: Исправьте свой код!

Прежде чем приступить к ускорению ваших сценариев с многозадачностью в PowerShell, следует выполнить несколько подготовительных шагов. В первую очередь оптимизируйте свой код.

Хотя вы можете выделить больше ресурсов для ускорения выполнения кода, многозадачность приносит дополнительную сложность. Если есть способы ускорить код до внедрения многозадачности, их следует реализовать в первую очередь.

Выявление узких мест

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

Пример общего способа ускорения кода – сместите фильтрацию влево. Если вы взаимодействуете с большим объемом данных, любую фильтрацию, которую вы хотите применить для уменьшения объема данных, следует выполнять как можно раньше. Приведен пример кода для получения количества используемого процессором svchost.

Приведенный ниже пример считывает все запущенные процессы, а затем фильтрует один процесс (svchost). Затем он выбирает свойство CPU и убеждается, что значение не является пустым.

PS51> Get-Process | Where-Object {$_.ProcessName -eq 'svchost'} | 
	Select-Object CPU | Where-Object {$_.CPU -ne $null}

Сравните этот код с примером ниже. Ниже приведен еще один пример кода, который выводит тот же результат, но организован по-другому. Обратите внимание, что код ниже проще и перемещает всю возможную логику влево от символа “|”. Это позволяет избежать возврата процессов, которые вам не интересны.

PS51> Get-Process -Name 'svchost' | Where-Object {$_.CPU -ne $null} | 
	Select-Object CPU

Ниже приведено различие во времени выполнения двух вышеприведенных строк. Временная разница в 117 мс не будет заметна, если вы запустите этот код только один раз, но она будет накапливаться, если код будет запущен тысячи раз.

time difference for running the two lines from above

Использование безопасного для потоков кода

Затем убедитесь, что ваш код является “безопасным для потоков”. Термин “безопасность для потоков” означает, что если один поток выполняет код, другой поток может выполнять тот же код одновременно и не вызывать конфликта.

Например, запись в один и тот же файл двумя разными потоками не является безопасной для потоков, так как неизвестно, что записывать в файл первым. В то время как два потока, считывающие из файла, являются безопасными для потоков, так как файл не изменяется. Оба потока получают одинаковый результат.

Проблема с многопоточным кодом PowerShell, который не является безопасным для потоков, заключается в том, что вы можете получить несогласованные результаты. Иногда он может работать нормально благодаря тому, что потоки случайным образом правильно синхронизируются, чтобы не вызывать конфликта. В других случаях у вас может возникнуть конфликт, и устранение проблемы будет затруднено из-за несогласованных ошибок.

Если вы выполняете всего две или три задачи одновременно, они могут случайно выстраиваться так, что все они записывают в файл в разное время. Затем, когда вы масштабируете код до 20 или 30 задач, вероятность того, что хотя бы две из них попробуют записать одновременно, сильно снижается.

Параллельное выполнение с PSJobs

Один из самых простых способов создания многозадачного сценария – использовать PSJobs. В PSJobs встроены cmdlet’ы из модуля Microsoft.PowerShell.Core. Модуль Microsoft.PowerShell.Core включен во все версии PowerShell с версии 3. Команды в этом модуле позволяют выполнять код в фоновом режиме, продолжая выполнение другого кода в переднем плане. Ниже вы можете увидеть все доступные команды.

PS51> Get-Command *-Job
Get-Command *-Job output

Отслеживание ваших задач

Все PSJobs находятся в одном из одиннадцати состояний. Эти состояния – способ управления задачами PowerShell.

Ниже вы найдете список наиболее распространенных состояний, в которых может находиться задача.

  • Завершено – Задача завершена, и данные вывода можно извлечь, или задачу можно удалить.
  • Запущено – Задача в настоящее время выполняется и не может быть удалена без принудительной остановки. Также еще нельзя извлечь вывод.
  • Заблокировано – Задача все еще выполняется, но перед тем, как продолжить, хосту требуется информация.
  • Ошибка – Во время выполнения задания произошла фатальная ошибка.

Чтобы получить статус задания, которое было запущено, используйте команду Get-Job. Эта команда возвращает все атрибуты ваших заданий.

Ниже приведен вывод для задания, где вы можете видеть, что состояние Завершено. Приведенный ниже пример выполняет код Start-Sleep 5 внутри задания с использованием команды Start-Job. Затем статус этого задания возвращается с помощью команды Get-Job.

PS51> Start-Job -Scriptblock {Start-Sleep 5}
PS51> Get-Job
Get-Job output

Когда статус задания возвращает Завершено, это означает, что код в блоке сценария выполнен и завершил свое выполнение. Вы также можете видеть, что свойство HasMoreData равно False. Это означает, что после завершения задания не было вывода для предоставления.

Ниже приведен пример некоторых других состояний, используемых для описания заданий. Вы можете видеть из столбца Command, что что-то могло вызвать неполноту выполнения некоторых заданий, например, попытка остановиться на abc секунд привела к неудавшемуся заданию.

All jobs

Создание новых заданий

Как вы видели выше, команда Start-Job позволяет создать новое задание, которое начинает выполнение кода в задании. При создании задания вы предоставляете блок сценария, который используется для задания. Затем PSJob создает задание с уникальным идентификатором и начинает выполнение задания.

Основное преимущество здесь заключается в том, что команда Start-Job выполняется быстрее, чем скрипт, который мы используем. Как видно на изображении ниже, вместо того чтобы выполняться пять секунд, команда занимает всего 0,15 секунды для запуска работы.

How long it takes to create a job

Причиной того, что код выполняется в доли времени, является то, что он выполняется в фоновом режиме как PSJob. На установку и запуск кода в фоновом режиме затрачивается всего 0,15 секунды, вместо того чтобы выполнять его в переднем плане и фактически ждать пять секунд.

Получение вывода работы

Иногда код внутри работы возвращает результат. Вы можете получить вывод этого кода, используя команду Receive-Job. Команда Receive-Job принимает вводом PSJob и выводит результат работы в консоль. Все, что было выведено работой во время ее выполнения, сохраняется, чтобы при получении работы выводить все, что было сохранено в это время.

Примером этого является выполнение следующего кода. Он создает и запускает работу, которая выводит Hello World. Затем он получает вывод работы и выводит его в консоль.

$Job = Start-Job -ScriptBlock {Write-Output 'Hello World'}
Receive-Job $Job
Receive-Job output

Создание запланированных задач

Еще один способ взаимодействия с PSJobs – это через запланированную задачу. Запланированные задачи аналогичны задачам, запланированным в Windows, и их можно настроить с помощью Планировщика задач. Запланированные задачи предоставляют способ запланировать выполнение сложных блоков сценариев PowerShell в задаче по расписанию. Используя запланированную задачу, вы можете запустить PSJob в фоновом режиме на основе триггеров.

Триггеры задач

Триггеры задач могут быть такими, как определенное время, вход пользователя, загрузка системы и многое другое. Также можно настроить повторение триггеров с интервалами. Все эти триггеры определяются с помощью команды New-JobTrigger. Эта команда используется для указания триггера, который будет запускать запланированную задачу. Запланированную задачу без триггера приходится запускать вручную, но у каждой задачи может быть много триггеров.

Помимо наличия триггера, у вас все равно должен быть блок сценария, аналогичный тому, что используется в обычной PSJob. Когда у вас есть и триггер, и блок сценария, вы используете команду Register-ScheduledJob, чтобы создать задачу, как показано в следующем разделе. Эта команда используется для указания атрибутов запланированной задачи, таких как блок сценария, который будет выполнен, и триггеры, созданные с помощью команды New-JobTrigger.

Демонстрация

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

Для этого сначала определите триггер, используя New-JobTrigger и определите запланированную задачу, как показано ниже. Эта запланированная задача будет записывать строку в файл журнала каждый раз, когда кто-то входит в систему.

$Trigger = New-JobTrigger -AtLogon
$Script = {"User $env:USERNAME logged in at $(Get-Date -Format 'y-M-d H:mm:ss')" | Out-File -FilePath C:\Temp\Login.log -Append}

Register-ScheduledJob -Name Log_Login -ScriptBlock $Script -Trigger $Trigger

После выполнения вышеуказанных команд вы получите выход, аналогичный созданию новой задачи, который покажет идентификатор задачи, скриптовый блок и некоторые другие атрибуты, как показано ниже.

Registering a scheduled job

После нескольких попыток входа вы можете видеть на скриншоте ниже, что они были зарегистрированы.

Example login.log text file

Использование параметра AsJob

Еще один способ использования задач – использовать параметр AsJob, который встроен во многие команды PowerShell. Поскольку существует множество различных команд, вы можете найти их все, используя Get-Command, как показано ниже.

PS51> Get-Command -ParameterName AsJob

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

Использование параметра AsJob делает то, что звучит, и запускает выполненную команду как задачу, а не синхронно в консоли.

Хотя большую часть времени AsJob можно использовать с локальной машиной, Invoke-Command не имеет встроенной опции для запуска на локальной машине. Существует обходной путь, используя Localhost в качестве значения параметра ComputerName. Ниже приведен пример этого обходного пути.

PS51> Invoke-Command -ScriptBlock {Start-Sleep 5} -ComputerName localhost

Чтобы показать параметр AsJob в действии, нижеприведенный пример использует Invoke-Command для задержки на пять секунд, а затем повторяет ту же команду, используя AsJob, чтобы показать разницу во времени выполнения.

PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5}}
PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5} -AsJob -ComputerName localhost}
Comparing speed of jobs

Runspaces: Как бы Потоки, только Быстрее!

До сих пор вы учились способам использования дополнительных потоков с помощью PowerShell, используя только встроенные команды. Еще один вариант для многопоточности вашего скрипта – использовать отдельное пространство запуска.

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

Пространство запуска против PSJobs

Хотя пространство запуска и PSJob имеют много общего, есть некоторые существенные различия в производительности. Самое большое различие между пространствами запуска и PSjobs – это время, необходимое для настройки и разборки каждого из них.

В примере из предыдущего раздела создание PSjob заняло около 150 мс. Это пример лучшего случая, поскольку блок сценария для работы не содержал много кода, и к работе не передавались дополнительные переменные.

В отличие от создания PSJob, пространство запуска создается заранее. Большинство времени, необходимого для запуска задания пространства запуска, обрабатывается до добавления какого-либо кода.

Ниже приведен пример выполнения той же команды, которую мы использовали для PSjob, в пространстве запуска вместо этого.

Measuring speed of job creation

В отличие от этого, ниже приведен код, используемый для версии runspace. Вы можете видеть, что для выполнения той же задачи требуется гораздо больше кода. Однако преимущество дополнительного кода позволяет сократить время выполнения на почти 3/4, что позволяет команде начать работу за 36 мс вместо 148 мс.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()
Measuring speed of creating a runspace

Запуск Runspaces: пошаговое руководство

Использование runspaces может быть сложной задачей сначала, так как в PowerShell больше нет команды hand-holding. Вам придется иметь дело с классами .NET напрямую. В этом разделе давайте разберем, что нужно сделать для создания runspace в PowerShell.

В этом пошаговом руководстве вы создадите отдельный runspace из своей консоли PowerShell и отдельный экземпляр PowerShell. Затем вы присвоите новый runspace новому экземпляру PowerShell и добавите код в этот экземпляр.

Создание Runspace

Первое, что вам нужно сделать, это создать новый runspace. Для этого используйте класс runspacefactory. Сохраните его в переменную, как показано ниже, чтобы в дальнейшем можно было ссылаться на него.

 $Runspace = [runspacefactory]::CreateRunspace()

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

 $PowerShell = [powershell]::Create()

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

 $PowerShell.Runspace = $Runspace
 $Runspace.Open()
 $PowerShell.AddScript({Start-Sleep 5})

Выполнение Runspace

До сих пор скриптблок не был запущен. Все, что было сделано, – это определение всего для пространства выполнения. Для запуска скриптблока у вас есть два варианта.

  • Invoke() – Метод Invoke() запускает скриптблок в пространстве выполнения, но ожидает возврата к консоли до тех пор, пока пространство выполнения не вернется. Это хорошо для тестирования, чтобы убедиться, что ваш код выполняется правильно, прежде чем позволить ему свободно работать.
  • BeginInvoke() – Использование метода BeginInvoke() – это то, что вам нужно, чтобы действительно увидеть прирост производительности. Это запустит скриптблок в пространстве выполнения и немедленно вернет вас к консоли.

При использовании BeginInvoke() сохраните вывод в переменную, так как это потребуется для просмотра состояния скриптблока в пространстве выполнения, как показано ниже.

$Job = $PowerShell.BeginInvoke()

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

the job is completed

Еще одна причина, по которой вам нужно сохранить вывод в переменной, заключается в том, что, в отличие от метода Invoke(), BeginInvoke() не возвращает автоматически вывод, когда код завершен. Для этого вы должны использовать метод EndInvoke(), когда он завершится.

В этом примере вывода не будет, но для завершения вызова вы можете использовать команду ниже.

$PowerShell.EndInvoke($Job)

После того как все задачи, которые вы поставили в очередь в пространстве запуска, завершатся, вы всегда должны закрыть пространство запуска. Это позволит автоматизированному процессу сбора мусора в PowerShell очищать неиспользуемые ресурсы. Ниже приведена команда, которую вы будете использовать для этого.

$Runspace.Close()

Использование пулов пространства запуска

Хотя использование пространства запуска действительно улучшает производительность, оно сталкивается с основным ограничением одного потока. Вот где пулы пространства запуска проявляют свое преимущество в использовании нескольких потоков.

В предыдущем разделе вы использовали только два пространства запуска. Вы использовали только одно для самой консоли PowerShell и одно, которое создали вручную. Пулы пространства запуска позволяют вам иметь несколько пространств запуска, управляемых на фоне с использованием одной переменной.

Хотя такое много-пространственное поведение может быть реализовано с помощью нескольких объектов пространства запуска, использование пула пространства запуска делает управление намного проще.

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

Рекомендуемое количество потоков в пуле пространства запуска зависит от количества выполняемых задач и машины, на которой выполняется код. Хотя увеличение максимального количества потоков в большинстве случаев не отрицательно влияет на скорость, вы также можете не увидеть никакой выгоды.

Демонстрация скорости пула пространства запуска

Чтобы продемонстрировать пример, когда пул потоков будет превосходить одиночный поток, предположим, что вы хотите создать десять новых файлов. Если бы вы использовали одиночный поток для этой задачи, вы создали бы первый файл, затем перешли ко второму, а затем к третьему и так далее, пока не были созданы все десять файлов. Пример скриптблока может выглядеть примерно так. Вы передаете этому скриптблоку десять имен файлов в цикле, и все они будут созданы.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

В приведенном ниже примере определяется блок скрипта, который содержит небольшой скрипт, который принимает имя и создает файл с этим именем. Создается пул потоков с максимальным количеством 5 потоков.

Затем цикл повторяется десять раз, и каждый раз ему будет присвоено значение номера итерации $_. Таким образом, на первой итерации будет значение 1, на второй – 2 и так далее.

Цикл создает объект PowerShell, присваивает ему блок скрипта и аргумент для скрипта, а затем запускает процесс.

Наконец, в конце цикла он ожидает завершения всех задач в очереди.

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
	$PowerShell = [powershell]::Create()
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.AddScript($ScriptBlock).AddArgument($_)
	$Jobs += $PowerShell.BeginInvoke()
}

while ($Jobs.IsCompleted -contains $false) {
	Start-Sleep 1
}

Теперь вместо создания потоков один за одним будут созданы пять потоков одновременно. Без пулов потоков вам бы пришлось создавать и управлять пятью отдельными потоками и пятью отдельными экземплярами Powershell. Управление этим быстро становится беспорядком.

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

Создание пулов потоков

Создание пула runspace очень похоже на runspace, созданный в предыдущем разделе. Ниже приведен пример того, как это сделать. Добавление scriptblock и вызов процесса идентичны runspace. Как видите ниже, пул runspace создается с максимальным количеством пяти потоков.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()

Сравнение Runspaces и Runspace Pools по скорости

Чтобы продемонстрировать разницу между runspace и runspace pool, создайте runspace и выполните команду Start-Sleep из предыдущего раздела. На этот раз, однако, ее нужно запустить 10 раз. Как видно в следующем коде, создается runspace, который будет спать 5 секунд.

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})

1..10 | Foreach-Object {
    $Job = $PowerShell.BeginInvoke()
    while ($Job.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100}
}

Обратите внимание, что поскольку вы используете единственный runspace, вам придется подождать, пока он завершится, прежде чем можно будет начать другой invoke. Поэтому добавлен sleep в 100 мс, пока задача не завершится. Хотя это можно уменьшить, вы увидите убывающую отдачу, так как будете тратить больше времени на проверку завершена ли задача, чем на ожидание завершения задачи.

Из приведенного ниже примера видно, что на выполнение 10 установок сон по 5 секунд заняло примерно 51 секунду.

Measuring performance of creating runspaces

Теперь, вместо использования одного runspace, перейдите к runspace pool. Ниже приведен код, который будет выполнен. Вы видите, что есть несколько различий в использовании двух в приведенном ниже коде при использовании runspace pool.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    $PowerShell.AddScript({Start-Sleep 5})
    $Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false) {Start-Sleep -Milliseconds 100}

Как видно ниже, это завершается чуть более чем за 10 секунд, что значительно лучше, чем 51 секунда для единственного runspace.

Comparing runspaces vs. runspace pools

Ниже приведено краткое изложение различий между runspace и runspace pool в этих примерах.

Property Runspace Runspace Pool
Wait Delay Waiting for each job to finish before continuing to the next. Starting all of the jobs and then waiting until they have all finished.
Amount of Threads One Five
Runtime 50.8 Seconds 10.1 Seconds

Введение в Runspaces с PoshRSJob

A frequent occurrence when programming is that you will do what is more comfortable and accept the small loss in performance. This could be because it makes the code easier to write or easier to read, or it could just be your preference.

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

Существует широко используемый модуль под названием PoshRSJob, который содержит модули, соответствующие стилю обычных PSJobs, но с дополнительным преимуществом использования runspaces. Вместо необходимости указывать весь код для создания runspace и объекта PowerShell, модуль PoshRSJob обрабатывает все это при выполнении команд.

Для установки модуля выполните следующую команду в административной сессии PowerShell.

Install-Module PoshRSJob

После установки модуля вы увидите, что команды такие же, как команды PSJob, с префиксом RS. Вместо Start-Job используется Start-RSJob. Вместо Get-Job используется Get-RSJob.

Ниже приведен пример того, как выполнить ту же команду с использованием PSJob, а затем снова с использованием RSJob. Как видите, у них очень похожий синтаксис и вывод, но они не совсем идентичны.

run the same command in a PSJob and then again in an RSJob

Ниже приведен некоторый код, который можно использовать для сравнения разницы в скорости между PSJob и RSJob.

Measure-Command {Start-Job -ScriptBlock {Start-Sleep 5}}
Measure-Command {Start-RSJob -ScriptBlock {Start-Sleep 5}}

Как видно ниже, есть значительная разница в скорости, поскольку RSJobs по-прежнему используют runspaces внутри.

large speed difference since the RSJobs are still using runspaces below the covers

Foreach-Object -Parallel

Сообщество PowerShell давно желало получить более простой и встроенный способ быстрого многопоточного выполнения процесса. В результате этого появился ключевой переключатель “parallel”.

На момент написания этого текста, PowerShell 7 все еще находится в предварительной версии, но к команде “Foreach-Object” был добавлен параметр “Parallel”. В этом процессе используются runspaces для параллельной обработки кода, а скриптблок, используемый для “Foreach-Object”, также используется как скриптблок для runspace.

Хотя детали все еще уточняются, это может стать более простым способом использования runspaces в будущем. Как видно ниже, вы можете быстро перебирать множество наборов задержек.

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}
Looping through and measuring commands

Проблемы с многопоточностью

Несмотря на то, что многопоточность звучит потрясающе, это не так просто. Большое количество проблем возникает при многопоточном выполнении любого кода.

Использование переменных

Одна из самых больших и очевидных проблем с многопоточностью заключается в том, что вы не можете использовать общие переменные без их передачи в качестве аргументов. Есть одно исключение – синхронизированная хэш-таблица, но это тема для другого разговора.

Как PSJobs, так и runspaces работают без доступа к существующим переменным, и нет способа взаимодействовать с переменными, используемыми в разных runspaces, из вашей консоли.

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

Для Start-Job и Start-RSJob из модуля PoshRSJob вы можете использовать параметр ArgumentList, чтобы предоставить список объектов, которые будут переданы в качестве параметров в скриптовый блок в том порядке, в котором вы их перечислили. Ниже приведены примеры команд, используемых для PSJobs и RSJobs.

PSJob:

Start-Job -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

RSJob:

Start-RSJob -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

Нативные пространства запуска не предоставляют вам того же удобства. Вместо этого вам придется использовать метод AddArgument() объекта PowerShell. Ниже приведен пример того, как это может выглядеть для каждого.

Пространство запуска:

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Хотя пулы пространств запуска работают одинаково, ниже приведен пример того, как добавить аргумент в пул пространства запуска.

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

Логирование

Многопоточность также вводит проблемы с логированием. Поскольку каждый поток работает независимо от других, они все не могут вести журнал в одном и том же месте. Если вы попытаетесь вести журнал, скажем, в файл с несколькими потоками, когда один поток пишет в файл, другие потоки не могут этого делать. Это может замедлить ваш код или привести к его полному отказу.

В качестве примера ниже приведен некоторый код для попытки ведения журнала 100 раз в один файл с использованием 5 потоков в пуле пространства запуска.

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
1..100 | Foreach-Object {
	$PowerShell = [powershell]::Create().AddScript({'Hello' | Out-File -Append -FilePath .\Test.txt})
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.BeginInvoke()
}
$RunspacePool.Close()

Из вывода вы не увидите ошибок, но если посмотрите на размер текстового файла, вы увидите, что не все 100 заданий завершились правильно.

Get-Content -Path .\Test.txt).Count” class=”wp-image-3241″/>
(Get-Content -Path .\Test.txt).Count

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

Другой вариант заключается в том, чтобы разрешить сбои времени вывода и регистрировать только то, что делала программа после завершения работы. Это позволяет сериализовать всё через вашу первоначальную сессию, но вы теряете некоторые детали, потому что не всегда знаете, в каком порядке происходили события.

Сводка

Хотя многопоточность может привести к значительному увеличению производительности, она также может вызвать головную боль. Хотя некоторые нагрузки могут получить значительную выгоду, другие совсем нет. Существует множество плюсов и минусов при использовании многопоточности, но если использовать её правильно, можно значительно сократить время выполнения кода.

Дальнейшее чтение

Source:
https://adamtheautomator.com/powershell-multithreading/