Вызов метода экземпляра, изолированного от актера, в синхронном контексте, изолированном от актера

У меня есть протокол актера, объявленный следующим образом:

protocol MyActorProtocol: Actor {
    func foo()
}

Есть актер, который соответствует протоколу:

actor MyImplementation1: MyActorProtocol {
    func foo() {}
}

Теперь мне нужно добавить прокси:

actor MyImplementation1Proxy: MyActorProtocol {
    let impl: MyActorProtocol
    init(impl: MyActorProtocol) {
        self.impl = impl
    }

    func foo() {
        // Error-1: Call to actor-isolated instance method 'foo()' in a synchronous actor-isolated context
        // impl.foo()


        // Error-2. 'await' in a function that does not support concurrency
        // await impl.foo() 


        // Success-3. only this passes the compiler check
        Task { await impl.foo() }
    }
}

Хочу понять такие моменты:

  1. Почему в протоколе актера могут быть объявлены неасинхронные методы без явного ключевого слова nonisolated?
  2. Допустим, возможны неасинхронные методы, тогда зачем мне вообще использовать Error-1 в моем коде?
  3. Учитывая, что MyImplementation1Proxy также соответствует MyActorProtocol, и MyImplementation1.foo должен быть вызван Task (по какой-либо причине), тогда кажется, что MyImplementation1.foo является «своего рода асинхронным», поэтому MyImplementation1Proxy.foo также должен иметь этот «своего рода асинхронный контекст», так почему у меня есть Error-2?
  4. Из Error-2 похоже, что метод просто «неасинхронный», но когда я попытался представить неакторную реализацию, получил Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context, что справедливо, но снова приводит к вопросу 1:
class MyImplementation2 {
   let impl: MyActorProtocol
   init(impl: MyActorProtocol) {
       self.impl = impl
   }

   func bar() {
       impl.foo()
   }
}

Заранее спасибо.

Почему протоколам актеров нельзя разрешать объявлять неасинхронные методы? Следует ли запретить актерам объявлять неасинхронные методы?

Sweeper 06.05.2024 13:26

@Sweeper «Почему протоколам актеров нельзя разрешать объявлять неасинхронные методы» - на самом деле это можно сделать с помощью ключевого слова nonisolated. Пример, который я опубликовал 1) не имеет ключевого слова nonisolated 2) НЕ является «неасинхронным», как доказывает ошибка MyImplementation2

olha 06.05.2024 13:35

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

Sweeper 06.05.2024 13:39

@Sweeper, спасибо за понимание «объединения изолированных и неизолированных и асинхронных и синхронизированных», проверю! Что касается «слишком многого», я спрашиваю о проблеме, которая кратко изложена в названии вопроса. Эти многочисленные вопросы просто исследуют проблему с разных сторон.

olha 06.05.2024 13:41

@Sweeper «изолированный против неизолированного и асинхронный против синхронизации. Это очень разные вещи» - позвольте мне попытаться уточнить: я спрашиваю, почему foo не является асинхронным в объявлении, но это не синхронизация - потому что MyImplementation2 не синхронизируется скомпилировать.

olha 06.05.2024 13:50

«в объявлении нет асинхронности, но это и не синхронизация». Это результат изоляции foo.

Sweeper 06.05.2024 14:22
Стоит ли изучать 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
6
1 275
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Почему протокол актера может иметь неасинхронные методы объявлено без явного неизолированного ключевого слова?

Actor методы по своей сути синхронны. Однако эти синхронные методы могут быть частью более широкой async процедуры. Actors изолируют свои методы, чтобы защитить их общее изменяемое состояние от доступа некоторых других параллельных процедур.

Мутация может произойти только из изолированного метода внутри Actor. Изолированные методы не могут быть вызваны из синхронного контекста вне Actor.

actor MyActor {

    var state = 0

    func foo() {
        state += 1
    }

}

let actor = MyActor()
actor.foo() // Error: Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context

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

actor MyActor {

    var state = 0

    nonisolated func foo() {
        state += 1 // Error: Actor-isolated property 'state' can not be mutated from a non-isolated context
    }

}

let actor = MyActor()
actor.foo()

Nonisolated будет работать только в том случае, если мы не будем изменять состояние Actor

actor MyActor {

    nonisolated func foo() {
        print("I'm not mutating here..")
    }

}

let actor = MyActor()
actor.foo()

Подводить итоги:

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

Допустим, возможны неасинхронные методы, тогда зачем мне когда-нибудь в моем коде была ошибка-1?

Потому что MyImplementation1Proxy.foo() синхронен, и вы вызываете там метод, изолированный от актера MyImplementation1.foo(), тем самым не соблюдая MyImplementation1 правила изоляции.

РЕДАКТИРОВАТЬ 1:

Учитывая, что MyImplementation1Proxy также соответствует MyActorProtocol, и MyImplementation1.foo должен быть вызван в Task (по какой-либо причине), тогда кажется, что MyImplementation1.foo является «своего рода асинхронным», поэтому MyImplementation1Proxy.foo должен иметь такой «вид асинхронного контекста», как ну так почему у меня Ошибка-2

MyImplementation1.foo не является асинхронным, как я уже говорил, «методы актера по своей сути синхронны». И так MyImplementation1Proxy.foo синхронно. Но их в любом случае нужно вызывать из асинхронного контекста, чтобы соблюдать их правила изоляции, если только это не так nonisolated. Я знаю, это немного сложно.

Если вы хотите использовать await внутри этих методов, очевидно, что вам нужно явно сделать их асинхронными, потому что это не так. Таким образом Error-2

nonisolated — плохой совет в данном случае. Просто заключите последние две строки в Task и await actor.foo().
vadian 06.05.2024 13:45

Спасибо, ваше объяснение вполне имеет смысл, но я считаю, что оно решает проблему // Error: Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context. А моя проблема связана с synchronous actor-isolated context, а это совсем другое. Другими словами, я знаю, как работать с ключевым словом nonisolated, но в моем коде вопроса это ключевое слово нигде не используется.

olha 06.05.2024 13:47

«Я знаю, это немного сложно». - спасибо, я думаю над этим абзацем...

olha 06.05.2024 14:07

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

Frankie Baron 06.05.2024 14:08
Ответ принят как подходящий

Вы объединяете изолированные и неизолированные и асинхронные и синхронизированные, которые являются ортогональными различиями.

Метод, изолированный от некоторого экземпляра актера, может быть синхронно вызван из контекста, изолированного от этого экземпляра актера. В противном случае его придется вызывать асинхронно, т. е. с помощью await. Вызов изолированного метода из контекста, не изолированного от этого актера, также включает в себя «переход актера», который имеет некоторые требования к Sendable, но я отвлекся.

Метод async должен везде вызываться асинхронно. Вы можете использовать await только в теле метода async.

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

actor Foo {
    func f1() { // non-async isolated
        
    }
    
    nonisolated func f2() { // non-async non-isolated
        
    }
    
    func f3() async { // async isolated
        
    }
    
    nonisolated func f4() async { // nonisolated async
        
    }
}

Вы не можете сделать await impl.foo() просто потому, что foo не является асинхронным (хотя и изолированным).

impl.foo() также недействителен, поскольку impl.foo изолирован от impl, но вы выполняете этот вызов из контекста, изолированного от self (прокси). Это разные актеры. Обратите внимание: это происходит не только потому, что их типы разные, но и потому, что self и impl — разные экземпляры. Это та же причина, по которой вы не можете сделать что-то вроде этого:

actor Foo {
    func foo() {
    }
    func bar() {
        // Foo().foo() is isolated to the new instance of Foo, not self!
        Foo().foo()
    }
}

Чтобы этот прокси работал, вам нужно убедить Swift, что, как только выполнение будет изолировано от self (MyImplementation1Proxy), оно также будет изолировано от impl.

Вам следует реализовать unownedExecutor , чтобы вернуть значение impl. Затем оберните вызов Предположим, изолирован.

actor MyImplementation1Proxy: MyActorProtocol {
    let impl: MyActorProtocol
    
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        impl.unownedExecutor
    }
    
    init(impl: MyActorProtocol) {
        self.impl = impl
    }

    func foo() {
        impl.assumeIsolated {
            $0.foo()
        }
    }
}

Когда кто-то вызывает MyImplementation1Proxy.foo, вызов выполняется MyImplementation1Proxy.unownedExecutor, который оказывается тем же исполнителем, что и тот, который используется impl. assumeIsolated проверяет, действительно ли текущий исполнитель impl.unownedExecutor, и запускает foo.

большое спасибо за такой замечательный ответ! У вас случайно нет ссылок на тему «также включает в себя «переход актера», который имеет некоторые требования к Sendable, но я отвлекся»? Я нашел только эту ссылку, но помимо того, что она слишком сложна для понимания, в ней ничего не упоминается о Sendable.

olha 06.05.2024 19:25
'assumeIsolated(_:file:line:)' is only available in iOS 17.0 or newer хотя
olha 06.05.2024 19:50

@olha в основном, если вы отправляете что-то из неизолированного контекста в изолированный контекст или между актерами, то, что вы отправляете, должно быть Sendable. Вы можете задать еще один вопрос по этому поводу, если сможете сформулировать его более конкретно.

Sweeper 07.05.2024 01:20

@olha Соответствующее предложение SE - вот это.

Sweeper 07.05.2024 04:25

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