Я пытаюсь добавить объект класса в качестве NoteProperty в другой класс внутри блока ForEach-Object -Parallel
в PowerShell 7. Однако, когда я пытаюсь вызвать метод для добавленного NoteProperty
, я получаю сообщение «Ссылка на объект не установлена на экземпляр». ошибки объекта.
Вот упрощенная версия моего кода:
class test {}
$test = [test]::New()
1..2 | ForEach-Object -Parallel {
class test2 {
testmethod () {
Write-Host "test method called"
}
}
Add-Member -InputObject $using:test -MemberType NoteProperty -Name "property$_" -Value ([test2]::New())
}
$test.property1.testmethod()
Если я выполню Get-Member $test.property1
, я получу метод testmethod
на выходе.
Я понимаю, что проблема может быть связана с тем, как объекты и свойства распределяются внутри параллельного блока. Как я могу правильно добавить NoteProperty и гарантировать, что вызов метода работает вне параллельного блока?
Ошибка, которую я получаю:
Получить членство на $test.property1
:
Это работает с простым ForEach-Object
без -Parallel
.
Переопределение пользовательского класса (в вашем случае class test2 { ... }
внутри каждой итерации параллельного foreach) имеет некоторые известные проблемы — см., например, https://github.com/PowerShell/PowerShell/issues/8767 и https:// github.com/PowerShell/PowerShell/issues/20893.
Вполне вероятно, что это еще один симптом той же основной проблемы.
Одним из обходных путей было бы объявление вашего класса в родительской области. Если вы это сделаете, вы сразу же обнаружите, что параллельный foreach не может получить доступ к этому типу в своей области, и вы получите ошибку Unable to find type [test2]
:
# broken code - don't use (see next code sample for a workaround)
class test {}
class test2 {
testmethod () {
Write-Host "test method called"
}
}
$test = [test]::New()
1..2 | ForEach-Object -Parallel {
Add-Member -InputObject $using:test -MemberType NoteProperty -Name "property$_" -Value ([test2]::New())
}
# Unable to find type [test2].
Но последующий обходной путь — зафиксировать ссылку на тип в переменной, а затем использовать модификатор области $using
для доступа к ней:
class test {}
class test2 {
testmethod () {
Write-Host "test method called"
}
}
$test2 = [test2]
# ^^^^^^^^^^^^^^
$test = [test]::New()
1..2 | ForEach-Object -Parallel {
Add-Member -InputObject $using:test -MemberType NoteProperty -Name "property$_" -Value (($using:test2)::New())
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ^^^^^^^^^^^^^^^^^^^^^^^
}
$test.property1.testmethod()
который дает результат
test method called
Это не было частью вашего первоначального вопроса, но имейте в виду, что вам необходимо убедиться, что ваш параллельный код является потокобезопасным - если несколько foreach -parallel
итераций попытаются запустить Add-Member
одновременно, вы можете получить непредсказуемые результаты.
У @SantiagoSquarzon есть хороший ответ, в котором описаны некоторые варианты координации нескольких потоков в PowerShell здесь - https://stackoverflow.com/a/75252238/3156906
Предлагаю добавить блокировку $using:test
, чтобы сделать ее потокобезопасной.
@SantiagoSquarzon - хорошая мысль. Я добавил небольшое обновление и указал на один из ваших предыдущих ответов :-)
Добавлю к полезному ответу Маклейтона:
Использование ForEach-Object -Parallel
(доступно только в PowerShell (Core) 7) сопряжено с рядом проблем:
Каждый параллельный поток выполняется в отдельном пространстве выполнения, которое, по сути, представляет собой независимый сеанс PowerShell, который ничего не знает о состоянии вызывающего объекта.
$using:
.Из-за параллельного выполнения вам, возможно, придется выполнить явную синхронизацию между пространствами выполнения (потоками), чтобы избежать состояний гонки или повреждения состояния.
Дополнительная проблема связана с использованием пользовательских class
PowerShell в разных средах выполнения, как в вашем случае:
Экземпляры PowerShell class
по умолчанию имеют привязку к пространству выполнения (т. е. они привязаны к определенному пространству выполнения), и попытка вызвать их члены из другого пространства выполнения может привести к повреждению состояния, симптомы которого варьируются от незаметных ошибок до явных ошибок, например, в вашей случай.
[scriptblock]
литералы ({ ... }
) также имеют сходство с пространством выполнения, и в их случае ForEach-Object -Parallel
явно предотвращает их использование в перекрестном пространстве выполнения;[1] например,$sb = { 'hi!' }; % -Parallel { & $using:sb }
вызывает следующую ошибку:A ForEach-Object -Parallel using variable cannot be a script block [...]
PowerShell 7.4+ предлагает способ создания class
без привязки к пространству выполнения, которые затем можно безопасно использовать из любого пространства выполнения, а именно через
[NoRunspaceAffinity()] атрибут.
Следующее решение:
[NoRunspaceAffinity()]
Add-Member
) за пределами блока ForEach-Object -Parallel
, в дополнительном обычном вызове ForEach-Object
, где не может возникнуть проблем с параллелизмом.Примечание:
class
внутри вызова ForEach-Object -Parallel
, потому что класс не только затем определяется в каждом параллельном пространстве выполнения, но и все результирующие классы тогда являются отдельными типами .NET.class test {}
$test = [test]::New()
1..2 | ForEach-Object -Parallel {
# Define the class without runspace affinity, so its instance
# can safely be used in other runspaces, notably the caller's.
[NoRunspaceAffinity()]
class test2 {
testmethod () {
Write-Host "test method called"
}
}
# Output an custom object with the target property name
# and an instance of the [test2] class
[pscustomobject] @{
Name = "Property$_"
Value = [test2]::New()
}
} | ForEach-Object {
# Attach an ETS NoteProperty member based on each runspace's output.
Add-Member -InputObject $test -MemberType NoteProperty -Name $_.Name -Value $_.Value
}
# OK - the [test2] instances stored as property values can now
# be safely accessed.
$test.property1.testmethod()
$test.property2.testmethod()
[1] By contrast, creating a script block from a string, using [scriptblock]::Create()
, creates an unbound[scriptblock]
instance, which runs in whatever runspace it is called from; however, ForEach-Object -Parallel
's refusal to use a script block via $using:
is categorical.
Скопируйте и вставьте сообщение об ошибке и информацию об участнике в вопрос в виде текста. Никаких фотографий.