Open14

SE0316: Global actors

UeeekUeeek

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を経由して同期的にアクセスすることで、データレースを防ぐ。

UeeekUeeek

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の正しい仕様を助ける。

UeeekUeeek

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
}
UeeekUeeek

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"に準拠するためには、型宣言と同じファイルで準拠させ、条件的な適応にすることはできない。

UeeekUeeek

The main actor

MainActorは MainThreadを表現するGlobalActorである

@globalActor
public actor MainActor {
  public static let shared = MainActor(...)
}
UeeekUeeek

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
    }
  }
}
UeeekUeeek

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に隔離されている場合である。

UeeekUeeek

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は、そのデータ競合を防ぐための一つの手段である。

UeeekUeeek

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に隔離されている必要がある

UeeekUeeek

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
}
UeeekUeeek

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
UeeekUeeek

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
UeeekUeeek

Source compatibility

追加機能であり、no impact on existing code