Open8

SE-0302: Concurrent Value and concurrent closures

UeeekUeeek

Introduction

Swift Concurrencyの目標は、"並行処理プログラミングの中の isolating stateの機能を提供することで、言語の機能として、データレースを 防ぐ”ことである。

このプロポーザルでは、以下の問題に挑戦する。
"Structured concurrencyとactoreの間の値のやり取りの型チェックをどう実現するか”
"Sendable"という名前のmarker protocolを使用して、実装する。
それは、関数にも同様に適応できる。

UeeekUeeek

Motivation

それぞれのActorとStructured Concurrency Taskは "single thread上の島”として表現できる、それらは、可変状態の集合を持つActorやTaskを、自然に同期する点となる。
AtcorやTaskは並行に実行されるが、私たちは、大部分のコードをsynchronizationしなくても良くしたい。
Actorの論理的独立を構築し、それをデータの同期店として使用する。(mailbox)

重要な疑問として、"いつ・どうやって 並行実行されるタスク間で、データのやり取りを許可するか”
データのやり取りは、Actor method callやTaskで発生します。

Swift Concurrencyは安全で強力なモデルを目指しています。以下の3点を実現したい。

  1. 安全でないshared mutable stateを 並行タスク間で共有しようとしたら、コンパイルエラーにする

  2. 並行処理の高度なアルゴリズムなどで、他者とって安全に使えるようなライブラリを実装できるようにする。

  3. 既存のコードからスムーズに移行できるようにする。
    以下では、モデル化したい一般的なケースと、その機会と挑戦を見ていく。

💖 Swift + Value Semantics

Intなどの簡単な型は、ポインター型ではないため、Concurrency domainを跨いだ受け渡しができる。
SwiftはValue semanticsを重視している。それは、concurrent boudariを跨いだ受け渡しが簡単にできるということである。
クラスを除くと、Swiftの型集約(type composition)は、value semanticsの機能を提供している。
これは、strucsやDictはconcurrency domainを跨いが受け渡しができる。

SwiftのCopyOnWrite(https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd)では、データの受け渡し時に、積極的なコピーを行わない。
これによって、Concurrency modelは実用面で効率的になる。

しかし、全てがシンプルなわけではない。
Collectionなどは、その要素にclassのreferenceやmutale stateにアクセスするclosureや 他のnon-value typeを含んでいるときに、安全ではなくなる。

Concurrency domainを跨いが受け渡しが、安全か安全でないかを判断できる必要がある。

Value Semantic Composition

Struct, Enumとtupleは 値の集合体の主要なモデルである。
これからは、集約しているデータがconcurrency domainを跨いが受け渡しを安全に行えるなら、集約したものも安全になる。
(structのメンバーの全てが安全 -> struct自身も安全になる)

Higher Order Functional Programming

Swiftでも、関数を他の関数に渡す、高階関数が使える。
FunctionはrefrenceTypeであるが、多くの関数は concurrencyDomainを跨いだ受け渡しに対して、安全である。例えば、何もキャプチャしない関数。

関数の形で、concurrencydomainを跨いだ 処理の受け渡しをする利点*はたくさんある。
下のactorの例を考える。

actor MyContactList {
  func filteredElements(_ fn: (ContactElement) -> Bool) async -> [ContactElement] {}
}

以下のように使える

// Closures with no captures are ok!
list = await contactList.filteredElements { $0.firstName != "Max" }

// Capturing a 'searchName' string by value is ok, because strings are
// ok to pass across concurrency domains.
list = await contactList.filteredElements {
  [searchName] in $0.firstName == searchName
}

concurrency domainを跨いだ関数の受け渡しは大切な機能であると考える。
が、local stateのreference型としての、captureを許さない方がいいのかもしれないとも思う。
また、安全でない型を値としてcaptureすることも許すべきでないと思う。
これらの二つは、メモリ安全の問題に関係する。

Immutable Classes

並行プログラミングのデザインパターンとして、immutableなデータ構造を作るというものがある。(よくある& 効率的)
それは、状態が変更されない限り、concurrency domanを跨いだ参照の受け渡しでも、完全に安全である。
このデザインパターンはとても効率的であり、"advanced data structure"を構築するために使え、純粋関数言語のコミュニティで広く議論されているものである。

Internally Synchronized Reference Types

並行プログラミングでよくあるデザインパターンとして、classを"thread safe"にする ものがある。
それらは、明示的に状態を同期させることで、安全にしている。
Actor instanceへのreferenceはこれの一つの具体例である。
Actorは concurrency domainを跨いだ ポインターとしての受け渡しに対して 安全である。
なぜなら、actorの中の状態は 非明示的だが、actor mailboxによって守られているからである。

“Transferring” Objects Between Concurrency Domains

とてもよくある、並行システムのパターンは、一つのconcurrency domainが 同期的でない変更可能な状態を構築して、それから、他のconcurreny domainにそれを raw pointerとして譲渡する方法あある。
もし送り主がそのデータを使用することをやめるなら、同期せずとも安全になる。(所有権の話?)
only sender or reciverがアクセスできる。

safeとunsafeの両方の方法でこれを実現できる。
AlternativeConsideration.exotic_typeで議論する。

Deep Copying Classes

安全なデータの受け私の方法の一つとして、データ構造をdeepCoyするものがある。
これは、sourceとdesinationのconcurrency domainで それぞれ mutable stateのコピーを持つことによって 安全性を実現する。
これは、巨大なデータ構造に対しては、コピーを作る分 重い。
だが、Objective-Cではよく使われていた手法である。
一般的な理解として、この機能は明示的であるべきである・。(expensiveだから?)

Motivation Conclusion

concurrency domainのデータの受け渡しに関する方法をいくつか紹介した。
Swiftのゴールとしては、ユーザに実装の中身を意識させることなく、安全に使えるAPIを提供することである。

UeeekUeeek

Proposed Solution + Detailed Design

概要として、"Sendable" marker protocolを進化させ、標準ライブラリをSendable対応し、@Sendable attributeを追加した。
それに加えて近い将来は、legacyなコードの互換性や、Objective-C framworksのfirst class supoprtも可能になるだろう。

Marker Protocols

"marker protocol"は、プロトコルが semantic propertyを持っていることを意味し、それはコンパイル時にチェックされるが、run-timeには影響がない。

"Marker protocol"には以下の制約がある。

  • any のような制約を持つことができない
  • non-marker protocolを継承することができない
  • "as?"などに、その名前を用いることができない。
  • non-marker-protocolの条件的な準拠のための、プロトコルのgeneric 制約に使用することができない。
    これは、将来便利になるが、現時点では、コンパイル時の機能としてのみにするべきである。
    "@_marker" attribute syntaxとしてつかう

Sendable Protocol

このプロポーザルのメインは、標準ライブラリに定義されるあるmarker protocolである。それは、特別な準拠のチェックルールを持つ

@_marker
protocol Sendable {}

複数のconcurrency domainを跨って使われるときに安全にするために、型がSendable protocolに準拠するのは良い考えである。
例えば、

コンパイラは、actor messageの送信や、concurrency callが"Sendable" protocolを準拠してないなら、それらをconcurrency domainを跨いで渡そうとしたときに、コンパイルエラーにする。

actor SomeActor {
  // async functions are usable *within* the actor, so this
  // is ok to declare.
  func doThing(string: NSMutableString) async {...}
}

// ... but they cannot be called by other code not protected
// by the actor's mailbox:
func f(a: SomeActor, myString: NSMutableString) async {
  // error: 'NSMutableString' may not be passed across actors;
  //        it does not conform to 'Sendable'
  await a.doThing(string: myString)
}

”Sendable"は、値をコピーすることによって、concurrency domainを跨って安全にデータを渡せることを可能にする型である。
注意として、正しく準拠しないと、バグを引き起こす可能性がある。そのため、compilerがチェックするようになっている。

Tuple conformance to Sendable

tupleの要素が全てSendableなら、タプルもsendableになるべきである。

Metatype conformance to Sendable

Metatypeはimmutableなので、常にSendableである。

UeeekUeeek

Sendable conformance checking for structs and enums

Sendableを集約させたものも、Sendableにできる。
例えば、Structsやenumも、Sendableの要素の集約ならSendableを準拠できる。

struct MyPerson : Sendable { var name: String, age: Int }
struct MyNSPerson { var name: NSMutableString, age: Int }

actor SomeActor {
  // Structs and tuples are ok to send and receive!
  public func doThing(x: MyPerson, y: (Int, Float)) async {..}

  // error if called across actor boundaries: MyNSPerson doesn't conform to Sendable!
  public func doThing(x: MyNSPerson) async {..}
}

これは便利に見える一方で、より検討が必要な場合は、少しprotocol準拠のfrictionを増やしたい。
例えば、enumやstructの要素が一つでもSendable出ないなら、そのenum/structがSendableに準拠しようとすると、コンパイラーはエラーを出す。

// error: MyNSPerson cannot conform to Sendable due to NSMutableString member.
// note: add '@unchecked' if you know what you're doing.
struct MyNSPerson : Sendable {
  var name: NSMutableString
  var age: Int
}

// error: MyPair cannot conform to Sendable due to 'T' member which may not itself be a Sendable
// note: see below for use of conditional conformance to model this
struct MyPair<T> : Sendable {
  var a, b: T
}

// use conditional conformance to model generic types
struct MyCorrectPair<T> {
  var a, b: T
}

extension MyCorrectPair: Sendable where T: Sendable { }

どの型も、"@unchecked"を"Sendable"につけることで、コンパイラーのチェックの挙動を上書きできる。
これは、その型がconcurrencyt domain間でも受け渡しにおいて安全であることを、プログラムを書く人が保証する必要がある。
"struct"と"enum"は、型が定義されたファイルと同じファイル内でのみ、 ”Sendable"を準拠できる。
これは、コンパイラーがチェックするときに、stored propertiesやaccosiated valuesがvisibleになるからである。

// MySneakyNSPerson.swift
struct MySneakyNSPerson {
  private var name: NSMutableString
  public var age: Int
}

// in another source file or module...
// error: cannot declare conformance to Sendable outside of
// the source file defined MySneakyNSPerson
extension MySneakyNSPerson: Sendable { }

この制約なしでは、private stored properyにアクセスできない、他のファイルやモジュールは、それらのprivateの変数のチェックができないため、誤ったSendableかどうかの判定をしてしまうことになる。
"unchecked"をつけるなら、他のファイルでSendable準拠させてもいい。(コンパイラーチェックしないから)

// in another source file or module...
// okay: unchecked conformances in a different source file are permitted
extension MySneakyNSPerson: @unchecked Sendable { }

Implicit struct/enum conformance to Sendable

structやenumは非明示的に"sendable"を準拠してる場合が多いが、明示的に"Sendable"と書く必要があり、ボイラープレートに感じる。
"usableFrominline"でないnon-publicなstructとenumで、frozen struct and enumについては、Sendableの準拠は非明示的に提供される。

struct MyPerson2 { // Implicitly conforms to Sendable!
  var name: String, age: Int
}

class NotConcurrent { } // Does not conform to Sendable

struct MyPerson3 { // Does not conform to Sendable because nc is of non-Sendable type
  var nc: NotConcurrent
}

Public non-frozen struct and enumは、非明示的な準拠にはならない。なぜなら、そうするとAPI resilienceの問題が生じるからである。(そうするように意図してなくても、SendableがAPIを使う側の制限になる。さらに、extendすることで うまくいかなくなる。)

Sendableの非明示的でない準拠は、non-generic typeと、instance dataがSendableであると保証されているgeneric typeで利用可能である。

struct X<T: Sendable> {  // implicitly conforms to Sendable
  var value: T
}

struct Y<T> {    // does not implicitly conform to Sendable because T does not conform to Sendable
  var value: T
}

Sendable conformance checking for classes

どんなクラスでも"@unchecked sendable"にすることで、concurrency domainを跨いだ受け渡しができる。これはクラスのメモリを安全に扱うためのアクセスコントロールとinternal syncとして適切である。
加えて、classでも ”unchecked”なしで利用できて、"Sendable"をチェックしてもらえる条件が存在する。
それは、final classで immutable stored propertyのみを持っている場合である。

final class MyClass : Sendable {
  let state: String
}

struct enum同様に、同じファイルでSendable準拠する必要がある。

Actor types

Actorは自身の内部でsyncする仕組みがあるので、非明示的にSendable準拠する。

Key path literals

KeypathはSendableを準拠する。
但し、keypath literalが Sendableを準拠した値をcaptureする場合のみである。

class SomeClass: Hashable {
  var value: Int
}

class SomeContainer {
  var dict: [SomeClass : String]
}

let sc = SomeClass(...)

// error: capture of 'sc' in key path requires 'SomeClass' to conform
// to 'Sendable'
let keyPath = \SomeContainer.dict[sc]
UeeekUeeek

New @Sendable attribute for functions

Function typeも、現状では Sendableでないreference typeである。
Functionsはいくつかのフォームがある

  • global func
  • nested func
  • accessors(getter, stter, subscripts)
  • closure

これらをSendableにできるならとても有益である。
"@Sendable" Attributeを導入する。
Function typeが concurrency domainを跨いで扱えるようになる。
メモリ安全を保証するために、コンパイラーが functionがSendableかをチェックする。

  1. functionがcaptureしている要素は全てSendableを準拠している必要がある。

  2. Sendable function TypeなClosureは by-value captureのみ可能。(let で宣言されたimmutable valuesはby-value capture, その他は明示的にcapture listで明記する必要がある。

    let prefix: String = ...
    var suffix: String = ...
    strings.parallelMap { [suffix] in prefix + $0 + suffix } // suffix はvarだから capture listで明記
    
 captureされる要素はすべてSendableである必要がある。
3. Accessorsは、Sendable systemでサポートされてない。現時点。
FunctionのSendable Attribute は、escapingとは直行している概念である。が、同じように働く。
```swift
actor MyContactList {
 func filteredElements(_ fn: @Sendable (ContactElement) -> Bool) async -> [ContactElement] { … }
}

上の例は下のように使える。

// Closures with no captures are ok!
list = await contactList.filteredElements { $0.firstName != "Max" }

// Capturing a 'searchName' string is ok, because String conforms
// to Sendable.  searchName is captured by value implicitly.
list = await contactList.filteredElements { $0.firstName == searchName }

// @Sendable is part of the type, so passing a compatible
// function declaration works as well.
list = await contactList.filteredElements(dynamicPredicate)

// Error: cannot capture NSMutableString in a @Sendable closure!
list = await contactList.filteredElements {
  $0.firstName == nsMutableName
}

// Error: someLocalInt cannot be captured by reference in a
// @Sendable closure!
var someLocalInt = 1
list = await contactList.filteredElements {
  someLocalInt += 1
  return $0.firstName == searchName
}

"Sendable" closureと"Sendable" typeの組み合わせは、拡張しやすい concurrency safeを可能にする。
その上、理解もできる簡単さである。

Inference of @Sendable for Closure Expressions

closureの"@Sendanble" attributeの規則は、"escaping"と似ている。
closureは以下のいずれか条件を満たすときに"@Sendable"になる。

  • Sendable function型と予測される文脈で使用している
  • closureの宣言に"@Sendable"と付いている。
    "escaping"との違いは、context-less closureは "Sendable"のデフォルトはNoだが、escapingはデフォルトでyes
// defaults to @escaping but not @Sendable
let fn = { (x: Int, y: Int) -> Int in x+y }

"Sendable" attributeは、nested functoin declarationsでも、使える。
コンパイラーにconcurrency safeのチェックをしてもらえるようになる。

func globalFunction(arr: [Int]) {
  var state = 42

  // Error, 'state' is captured immutably because closure is @Sendable.
  arr.parallelForEach { state += $0 }

  // Ok, function captures 'state' by reference.
  func mutateLocalState1(value: Int) {
    state += value
  }

  // Error: non-@Sendable function isn't convertible to @Sendable function type.
  arr.parallelForEach(mutateLocalState1)

  @Sendable
  func mutateLocalState2(value: Int) {
    // Error: 'state' is captured as a let because of @Sendable
    state += value
  }

  // Ok, mutateLocalState2 is @Sendable.
  arr.parallelForEach(mutateLocalState2)
}

Thrown errors

違うconcurrency domainから関数が呼ばれていたとしても、thros errorの値を渡すことができる。A

class MutableStorage {
  var counter: Int
}
struct ProblematicError: Error {
  var storage: MutableStorage
}

actor MyActor {
  var storage: MutableStorage
  func doSomethingRisky() throws -> String {
    throw ProblematicError(storage: storage)
  }
}

"doSomethinkRisky()"を別のconcurrency domainから呼ぶと、"Problematic Error"が投げられる。
それは、"myActor"のmutable stateをキャプチャしている。
そして、それが他のconcurrency domainに渡され、actor isolationに違反することとなる。
Throwする型を明記する方法がないため、Sendable checkすることができない。
全てのErrorをSendableにすることによって、これを解決することができる。

protocol Error: Sendable {}

こうすることで、上記のコードは mutable state"がSendableではないため、エラーとなる。
一般的に、新しいプロトコルに準拠させることは互換性の問題が起こるが、
Marker protocolなので、問題が発生しない。
Swift<6では、ErrorのSendable checkは エラーでなく、warningとなる。
(https://developer.apple.com/documentation/swift/error)

Adoption of Sendable by Standard Library Types

Standard LibraryへのSendableの対応も重要である。

extension Int: Sendable {}
extension String: Sendable {}

Generic Value-semantic Typesも要素がSendableならSendableにできる。

extension Optional: Sendable where Wrapped: Sendable {}
extension Array: Sendable where Element: Sendable {}
extension Dictionary: Sendable
    where Key: Sendable, Value: Sendable {}

以下の例外を除いて、standard libraryの全てのstruct. enum. classはSendableに準拠できる。
Generic Typesは要素がSendableという条件下でSendableに準拠できる。
例外的なルールは以下である。

  • ManagedBuffer: このクラスは、バッファにmutable refenreceを提供する. sendableにできない。
  • Unsafe(Mutable)(Buffer)Pointer: Sendableになっているが、安全性はプログラマが保証する必要がある。
  • Lazy algorithm adapter types:

ErrorとCodingKeyはSendable

  • Error: Concurrency domainを跨いでThrowするため
  • CodingKey: EncodingErrorとDecodingErrorをSendableにするため。

Support for Imported C / Objective-C APIs

Cのいつくかの型にSendableを適応させておくことで、基本的なルールは準拠させる?

  • C enum types always conform to the Sendable protocol.
  • C struct types conform to the Sendable protocol if all of their stored properties conform to Sendable.
  • C function pointers conform to the Sendable protocol. This is safe because they cannot capture values.
UeeekUeeek

Source Compatibility

ほとんど完全に、compatible.
ただのmarkerなので使わなくても影響がない。
いくつかエッジケースがある。

  • The change to keypath literals subscripts will break exotic keypaths that are indexed with non-standard types.
  • Error and CodingKey inherit from Sendable and thus require that custom errors and keys conform to Sendable.
    上記の点において、エラーとなる。
    Swift6からエラー。Swift5ではwarning.
UeeekUeeek

Conclusion

This proposal defines a very simple approach for defining types that are safe to transfer across concurrency domains. It requires minimal compiler/language support that is consistent with existing Swift features, is extensible by users, works with legacy code bases, and provides a simple model that we can feel good about even 20 years from now.

Because the feature is mostly a library feature that builds on existing language support, it is easy to define wrapper types that extend it for domain specific concerns (along the lines of the NSCopied example above), and retroactive conformance makes it easy for users to work with older libraries that haven’t been updated to know about the Swift Concurrency model yet.