У меня есть протокол актера, объявленный следующим образом:
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() }
}
}
Хочу понять такие моменты:
nonisolated
?Error-1
в моем коде?MyImplementation1Proxy
также соответствует MyActorProtocol
, и MyImplementation1.foo
должен быть вызван Task
(по какой-либо причине), тогда кажется, что MyImplementation1.foo
является «своего рода асинхронным», поэтому MyImplementation1Proxy.foo
также должен иметь этот «своего рода асинхронный контекст», так почему у меня есть Error-2
?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 «Почему протоколам актеров нельзя разрешать объявлять неасинхронные методы» - на самом деле это можно сделать с помощью ключевого слова nonisolated
. Пример, который я опубликовал 1) не имеет ключевого слова nonisolated
2) НЕ является «неасинхронным», как доказывает ошибка MyImplementation2
Я вижу, в чем ваше непонимание. Вы путаете изолированные и неизолированные и асинхронные и синхронизированные. Это очень разные вещи. Вам следует внимательно прочитать раздел Параллелизм руководства по языку. Также вам следует отредактировать свой вопрос, чтобы не задавать так много всего в одном посте.
@Sweeper, спасибо за понимание «объединения изолированных и неизолированных и асинхронных и синхронизированных», проверю! Что касается «слишком многого», я спрашиваю о проблеме, которая кратко изложена в названии вопроса. Эти многочисленные вопросы просто исследуют проблему с разных сторон.
@Sweeper «изолированный против неизолированного и асинхронный против синхронизации. Это очень разные вещи» - позвольте мне попытаться уточнить: я спрашиваю, почему foo
не является асинхронным в объявлении, но это не синхронизация - потому что MyImplementation2
не синхронизируется скомпилировать.
«в объявлении нет асинхронности, но это и не синхронизация». Это результат изоляции foo
.
Почему протокол актера может иметь неасинхронные методы объявлено без явного неизолированного ключевого слова?
Actor
методы по своей сути синхронны. Однако эти синхронные методы могут быть частью более широкой async
процедуры. Actor
s изолируют свои методы, чтобы защитить их общее изменяемое состояние от доступа некоторых других параллельных процедур.
Мутация может произойти только из изолированного метода внутри 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()
.
Спасибо, ваше объяснение вполне имеет смысл, но я считаю, что оно решает проблему // Error: Call to actor-isolated instance method 'foo()' in a synchronous nonisolated context
. А моя проблема связана с synchronous actor-isolated context
, а это совсем другое. Другими словами, я знаю, как работать с ключевым словом nonisolated
, но в моем коде вопроса это ключевое слово нигде не используется.
«Я знаю, это немного сложно». - спасибо, я думаю над этим абзацем...
Я считаю, что обе проблемы на самом деле могут иметь одну и ту же основную причину — вызов некоторого изолированного от актера метода из синхронного контекста.
Вы объединяете изолированные и неизолированные и асинхронные и синхронизированные, которые являются ортогональными различиями.
Метод, изолированный от некоторого экземпляра актера, может быть синхронно вызван из контекста, изолированного от этого экземпляра актера. В противном случае его придется вызывать асинхронно, т. е. с помощью 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
.
'assumeIsolated(_:file:line:)' is only available in iOS 17.0 or newer
хотя
@olha в основном, если вы отправляете что-то из неизолированного контекста в изолированный контекст или между актерами, то, что вы отправляете, должно быть Sendable
. Вы можете задать еще один вопрос по этому поводу, если сможете сформулировать его более конкретно.
@olha Соответствующее предложение SE - вот это.
Почему протоколам актеров нельзя разрешать объявлять неасинхронные методы? Следует ли запретить актерам объявлять неасинхронные методы?