SE0316: Global actors
Introduction
Actorは、内部のデータをconcurrent accessから守る 新しい reference型である。
SwiftのActorでは、上記の挙動を (コンパイル時に)内部のデータへの全てのアクセスを、直列実行する同期メカニズムを通して行うようにするActor islolationによって、それを可能にした。
ここでは、GlobalActor (single actorタイプの外に actor isolationを拡張する)を導入する。
それによって、状態や関数が多くの方や関数やモジュールに散らばっていても、global stateとそれにアクセスする関数に利点がある。
Global Actorは、concurrent programでglobal variablesが安全に動くことを可能にする。
Global Actorは global やstatic variableに global actorを経由して同期的にアクセスすることで、データレースを防ぐ。
Motivation
Actorは以下の点がすごい
- データを隔離できる
- concurrent programでデータ競合を起こさずに参照型を扱える手段を提供する
しかし、隔離したいデータがプログラム上で散らばっているときに、もしくは、プログラムの外に存在する状態を表現したいときに、そのようなコードやデータを一つのアクターにまとまることは、実用的ではない もしくは不可能である。(ここで言ってるのは、大きいプロジェクトの場合)
Global Actorの主要な動機は、mainThread からのみアクセス可能な状態や処理に対して、actor modelを適応することである。
applicationでは、MianThreadは 通常、さまざまな入力を処理するevent-handling loopを実行している。
画面のあるアプリでは、よく user-interactionがmainThreadで実行され、状態の変更もmainThreadで実行する必要がある。
Global ActorはMainThreadをActorとして使用し、actor isolationによってmainThreadの正しい仕様を助ける。
Proposed Solution
GlobalActorは、型によって特定されるGlobally-uniqueなActorである。
その型はcustom attributeとなる。
どんな宣言でも、attributeとしてglobal actor typeをつけることで、actor-isolatedにできる。
その時点で、通常のActor-isolation制限が適応されます。
宣言することで、他の同じGlobalActorの宣言から、同機的なアクセスしか受け付けなくなる。
例えば、MainThreadのGlobalActorとしてMainActorを導入する。それは、関数がMainThreadでのみ実行されるように制限かけれる。
@MainActor var globalTextSize: Int
@MainActor func increaseTextSize() {
globalTextSize += 2 // okay:
}
func notOnTheMainActor() async {
globalTextSize = 12 // error: globalTextSize is isolated to MainActor
increaseTextSize() // error: increaseTextSize is isolated to MainActor, cannot call synchronously
await increaseTextSize() // okay: asynchronous call hops over to the main thread and executes there
}
Defining global actors
GlobalActorは "@globalActor" attributeのついた型である。
sharedとついたstatic propertyを持ち、それがactorのshared instanceとして提供される。
@globalActor
public struct SomeGlobalActor {
public actor MyActor { }
public static let shared = MyActor()
}
struct, enum,. actor. final classが Global actorになることができる。
sharedを経由してshared actorにアクセスを提供する marker typeである。
Shared instanceは GlobalActorタイプのglobally-unique instance隣、GlobalActorとして宣言されたコードやデータに対する同期的アクセスをするために使用される。
GlobalActorsは GlobalActor プロトコルを暗黙的に準拠する。
GlobalActor protocolは"Shared"を持つように定めている。
"@globalActor"に準拠するためには、型宣言と同じファイルで準拠させ、条件的な適応にすることはできない。
The main actor
MainActorは MainThreadを表現するGlobalActorである
@globalActor
public actor MainActor {
public static let shared = MainActor(...)
}
Using global actors on functions and data
最初の例で説明したように、functionもデータもGlobalActorとしてAttributeをつけ、GlobalActorとして隔離することができる。
GlobalActorは最初の例のように、GlobalFunctionやデータに限定されているわけではない。
クラスのメンバーの型やプロトコルをGlobalActorに属させることもできる。
例えば、Notificationを受け取った時の処理は、MainThreadで行うことが期待される。
それゆえ、そのmethodと関係するデータはMainActorになる必要がある。
ここでは、ViewControllerの例の一部を紹介する
class IconViewController: NSViewController {
@MainActor @objc private dynamic var icons: [[String: Any]] = []
@MainActor var url: URL?
@MainActor private func updateIcons(_ iconArray: [[String: Any]]) {
icons = iconArray
// Notify interested view controllers that the content has been obtained.
// ...
}
}
VCの中のデータは、データを更新するmethodと同様に、MainActorに隔離されている。
これは、ViewControllerのUIの更新がmainThreadのみで起こることを保証する、その他のThreadからアクセスしようとするとcompile errorとなる。
GlobalActors を使ってurlの更新処理を書くと、以下のような例が考えられる、
@MainActor var url: URL? {
didSet {
// Asynchronously perform an update
Task.detached { [url] in // not isolated to any actor
guard let url = url else { return }
let newIcons = self.gatherContents(url)
await self.updateIcons(newIcons) // 'await' required so we can hop over to the main actor
}
}
}
Global actor function types
Synchronous関数型は、その関数が特定のglobalActor型の呼び出しのみになるように、することができる。
var callback: @MainActor (Int) -> Void
そのような関数は、同じglobalActorに隔離されたコードしか同期的に呼び出されない。
GlobalActorに隔離された関数への参照も GlobalActorの関数型になる。
参照自体はActor isolationのチェック対象ではない、なぜならActor isolationは結果として得られる関数型によって記述されるから。?(参照を渡すときではなく、呼び出すときに、Actor isolated チェックされそう)
例えば
func functionsAsValues(controller: IconViewController) {
let fn = controller.updateIcons // okay, type is @MainActor ([[String: Any]]) -> Void
let fn2 = IconViewController.controller.updateIcons // okay, type is (IconViewController) -> (@MainActor ([[String: Any]]) -> Void)
fn([]) // error: cannot call main actor-isolated function synchronously from outside the actor
}
GlobalActorのついてない関数型は、GlobalActorのついた関数型に変換することができる。
func acceptInt(_: Int) { } // not on any actor
callback = acceptInt // okay: conversion to @MainActor (Int) -> Void
反対の変換は、synchronous functionには許されていない(GlobalActorのついた関数を、ついてないものに変換する)
なぜなら、そうすると、GlobalActorなしでその関数が呼ばれてしまうことが可能になるから。
let fn3: (Int) -> Void = callback // error: removed global actor `MainActor` from function type
しかし、async functionに変換されるときは、GlobalActorのついた関数から、 GlobalActorのついてない関数に変換することができる。
この場合は、その非同期関数のbodyが実行される前に、GlobalActorに"hop"する。(ContextをSwitchするイメージそう)
let callbackAsynchly: (Int) async -> Void = callback // okay: implicitly hops to main actor
これは、下のコードの糖衣構文である。
let callbackAsynchly: (Int) async -> Void = {
await callback() // `await` is required because callback is `@MainActor`
}
関数型のGlobalActorQualifierは @Sendable, Async, Throwsや他のAttribute、Modifierとは独立したものである。
唯一の例外は、関数自身が、instance actorに隔離されている場合である。
Closures
Closureでは、Attributeをつけることで、GlobalActorに隔離されることを明確にすることができる。
callback = { @MainActor in
print($0)
}
callback = { @MainActor (i) in
print(i)
}
GlobalActorがclosureにつけられたとき、closureの型は、GlobalActorがついたものになる。
DispatchQueue.main.async{}の代わりに使用することができる。
Task.detached { @MainActor in
// ...
}
この書き方は、closureのbodyがMainActorで実行されることを保証し、他の"MainAcot-annotated"な関数などを同期的に呼び出すことができる
closureが、パラメータやGlobalActorのついた関数型の値の初期化に使用され、closureにGlobalActorが明確につけられてないなら、そのClosureはそのGlobalActorがついていると推測される。
@MainActor var globalTextSize: Int
var callback: @MainActor (Int) -> Void
callback = { // closure is inferred to be @MainActor due to the type of 'callback'
globalTextSize = $0 // okay: closure is on @MainActor
}
Global and static variables
Global and Static variablesは、GlobalActorのannotationをつけることができる。
そのような変数は、同じ GlobalActorからの同期的アクセスか、非同期のアクセスのみを受け付ける。
@MainActor var globalCounter = 0
@MainActor func incrementGlobalCounter() {
globalCounter += 1 // okay, we are on the main actor
}
func readCounter() async {
print(globalCounter) // error: cross-actor read requires 'await'
print(await globalCounter) // okay
}
Actorを跨いだ参照には、Sendableを準拠させる必要がある。
GlobalActorのアノテーションがされていないGlobal and static variablesは、どのconcurrency contextからもアクセスすることができる。そのため、データ競合が発生しやすくなる。
GlobalActorは、そのデータ競合を防ぐための一つの手段である。
Using global actors on a type
型全体やクラスでさえもMainThreadで実行することを誓約にすることは一般的である。そして、非同期的な実行も特別なケースである。
そのような場合で、型自身をGlobalActorとしてしてアノテート出来る。そして、全てのmethod, properyそしてsubscriptsも GlobalActorに隔離される。
その型の全てのメンバーは、もしGlobalActorに隔離したくないのであれな、nonisolated modifierによってopt-outできる。
@MainActor
class IconViewController: NSViewController {
@objc private dynamic var icons: [[String: Any]] = [] // implicitly @MainActor
var url: URL? // implicitly @MainActor
private func updateIcons(_ iconArray: [[String: Any]]) { // implicitly @MainActor
icons = iconArray
// Notify interested view controllers that the content has been obtained.
// ...
}
nonisolated private func gatherContents(url: URL) -> [[String: Any]] {
// ...
}
}
GlobalActorとアノテートされた Protocolでない型は、Sendableに準拠する。
そのような型のインスタンスは、concurrency domainを跨いで安全に共有できる。なぜなら、stateはGlobalActorを経由してのみアクセスできるからである。
親クラスがない/親クラスも同じGlobalActorになっている/親がNSObjectなら、classもGlobalActorとしてアノテートできる。
Subclassは、同じGlobalActorに隔離されている必要がある
Global actor inference
明示的に GlobalActorやnonisolatedとisolatedとアノテーとされてない宣言も、いつくかの場所では推測することができる。
- Subclassは親クラスから推測できる
class RemoteIconViewController: IconViewController { // implicitly @MainActor
func connect() { ... } // implicitly @MainActor
}
- overrideするときは、override元のGlobalActorになる。
class A {
@MainActor func updateUI() { ... }
}
class B: A {
override func updateUI() { ... } // implicitly @MainActor
}
- ActorTypeの中にないWitnessは、witnessとしてのプロトコルへの準拠が同じ方定義かextension内に明記されているなら、protocol Requirementsから推測することができる、
protocol P {
@MainActor func f()
}
struct X { }
extension X: P {
func f() { } // implicitly @MainActor
}
struct Y: P { }
extension Y {
func f() { } // okay, not implicitly @MainActor because it's in a separate extension
// from the conformance to P
}
- GlobalActorのProtocolに準拠する NonActorTypeは、同じソースファイル内なら推論できる。
@MainActor protocol P {
func updateUI() { } // implicitly @MainActor
}
class C: P { } // C is implicitly @MainActor
// source file D.swift
class D { }
// different source file D-extensions.swift
extension D: P { // D is not implicitly @MainActor
func updateUI() { } // okay, implicitly @MainActor
}
+GlobalActorのついたwrappedPropertyを持つStruct/class
@propertyWrapper
struct UIUpdating<Wrapped> {
@MainActor var wrappedValue: Wrapped
}
struct CounterView { // infers @MainActor from use of @UIUpdating
@UIUpdating var intValue: Int = 0
}
Global actors and instance actors
宣言は、GlobalActorとInstanceActorの両方に隔離することはできない。
GlobalActorにアノテートしてインスタンスを生成したなら、GlobalActorに隔離され、Actor instance自身のではなくなる。
actor Counter {
var value = 0
@MainActor func updateUI(view: CounterView) async {
view.intValue = value // error: `value` is actor-isolated to `Counter` but we are in a 'MainActor'-isolated context
view.intValue = await value // okay to asynchronously read the value
}
}
With the isolated parameters described in SE-0313, no function type can contain both an isolated parameter and also a global actor qualifier:
"isolated parameter"で、function typeでないパラメータは isolated parameterとGlobalActor annotatoinの両方を持てる。
@MainActor func tooManyActors(counter: isolated Counter) { } // error: 'isolated' parameter on a global-actor-qualified function
Detailed design
GlobalActor attributesは宣言時に、以下のように適応できる。
-
宣言では、複数のGlobalActor attributesを持つことはできない。下のルール(いくつかのケースで、GlobalActor attributesは宣言から他の宣言へ、伝播する)と言っている。
- そのルールがもし、defaultで伝播すると言っていたなら、対象の宣言がすでにGlobalActor Attributeを明示的に持っているなら伝播は起きない。
- そのルールがもし、"必ず伝播する"と言っているなら、対象の宣言がすでにGlobalActor Attributeを持っているならエラーとなる。(同一のGlobalActorなら大丈夫)
-
GlobalActor Attributeで宣言された関数は、与えられたGlobalActorに隔離される。
-
GlobalActorのついたStoredVariableや定数は、与えられたGlobalActorに隔離される。
-
GlobalActorのついた変数へのアクセスやSubscriptは、GlobalActorがついたものとして宣言される。
-
Lobal Variableや定数は、GlobalActorをつけることができない。
-
GlobalActor付きで宣言された型は、全ての関数、property, subscriptsとextensionが defaultでGlobalActor Attribtueが伝播する。
-
GlobalActorがついて宣言されたextensionは、そのAttributeが全てのメンバーにdefaultで伝播する。 (Classに付けずにExtensionにだけ、つけるとかできる。
-
GlobalActor Attributeがついて宣言されたprotocolは、デフォルトで、それを準拠する型に Attributeを伝播させる。
-
GlobalActor付きのProtoco制約は、特定のwitnessが同じglobalActorを持つか、Actorに隔離されてない必要がある。( これは、all-witness for actor-isloated制約と同じ)
-
GlobalActor付きで宣言されたClassは、必ず、subclassにそのattributesが伝播する。
-
overrideすると、元の関数の GlobalActor Attributeがあるなら、それが必ず伝播する。他の形式のPropagateはoverrideに適応してはならない。もし、GlobalActor付きの宣言が、何もAttirbuteのついてない関数をoverrideしたら、エラーとなる。
-
Actor型はGlobalActorになることはできない。Atcorのstored instanceは GlobalActorAttributeを持つことはできない。Actorの他のメンバーはGlobalActor Attributeを持つことができる。そのようなメンバーはそのGlobalActorに隔離される。
-
DeinitはGlobalActor Attributeを持つことができないし、Propagationの対象になることはない。
GlobalActor protocol
GlobalActor protocolは以下のように宣言される。
/// A type that represents a globally-unique actor that can be used to isolate
/// various declarations anywhere in the program.
///
/// A type that conforms to the `GlobalActor` protocol and is marked with the
/// the `@globalActor` attribute can be used as a custom attribute. Such types
/// are called global actor types, and can be applied to any declaration to
/// specify that such types are isolated to that global actor type. When using
/// such a declaration from another actor (or from nonisolated code),
/// synchronization is performed through the \c shared actor instance to ensure
/// mutually-exclusive access to the declaration.
public protocol GlobalActor {
/// The type of the shared actor instance that will be used to provide
/// mutually-exclusive access to declarations annotated with the given global
/// actor type.
associatedtype ActorType: Actor
/// The shared actor instance that will be used to provide mutually-exclusive
/// access to declarations annotated with the given global actor type.
///
/// The value of this property must always evaluate to the same actor
/// instance.
static var shared: ActorType { get }
}
Closure attributes
GlobalActor Attributeは、いくつかあるClosureに適応できるAttributeの一つである。
closure-expression → { closure-signature opt statements opt }
closure-signature → attributes[opt] capture-list[opt] closure-parameter-clause async[opt] throws[opt] function-result[opt] in
closure-signature → attributes[opt] capture-list in
closure-signature → attributes in
Source compatibility
追加機能であり、no impact on existing code