«Ссылка на объект не установлена ​​на экземпляр объекта» при вызове метода NoteProperty, добавленного в параллельную обработку

Я пытаюсь добавить объект класса в качестве 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.

Скопируйте и вставьте сообщение об ошибке и информацию об участнике в вопрос в виде текста. Никаких фотографий.

lit 24.06.2024 12:46
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
138
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Переопределение пользовательского класса (в вашем случае 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, чтобы сделать ее потокобезопасной.

Santiago Squarzon 24.06.2024 20:19

@SantiagoSquarzon - хорошая мысль. Я добавил небольшое обновление и указал на один из ваших предыдущих ответов :-)

mclayton 24.06.2024 20:33

Добавлю к полезному ответу Маклейтона:

Использование 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()]
  • позволяет избежать необходимости синхронизации, обрабатывая оформление свойства ETS через (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.

Другие вопросы по теме