WWDC2021
Concurrency with Swift
Platforms State of the Union
Structured Concurrency
(from Platforms State of the Union, 23: 53)
func prepareForShow() async throws -> Scene {
async let dancers = danceCompany.warmUp(duration: .minutes(45))
async let scenery = crew.fetchStageScenery()
let openingScene = setStage(with: await scenery)
return try await dancers.moveToPosition(in: openingScene)
}
Actors
Definition
actor StageManager {
var stage: Stage
func setStage(with scenery: Scenery) -> Scene {
stage.backdrop = scenery.backdrop
for prop in scenery.props {
stage.addProp(prop)
}
return stage.currentScene
}
}
Usage from external context
it needs await
keyword to use an actor's method.
let scene = await stageManager.setStage(with: scenery)
DispatchQueue.main
To indicate the execution in the main thread, now we use DispatchQueue.main.async { }
. It enables us to use actors in main thread to add @MainActor
keyword.
@MainActor
func display(scene: Scene
await display(scene: scene)
Async Sequence
for try await line in url.lines {
//process each line
}
Xcode Cloud
so-called half modal view
Customize and resize sheets in UIKit
SwiftUI
What's new in Swift
冒頭はコミュニティ関係のトピック、ビルド時間の短縮、ドキュメンテーション。
DocC
DocCのセッションが4つも用意されている:
このセッションで予告されていた、オープンソース化の結果が
らしい。
Ergonomic improvements
続いて、Swift Evolutionで議論の結果追加された機能。
Flexible static member lookup for Generic parameter
// Flexible static member lookup
// SE-0297 SE-0299
protocol Coffee {}
struct RegularCoffee: Coffee {
var tall: RegularCoffee { self }
}
struct Cappuccino: Coffee {
var large: Cappuccino { self }
}
extension Coffee where Self == RegularCoffee {
static var regular: RegularCoffee { RegularCoffee() }
}
extension Coffee where Self == Cappuccino {
static var cappucino: Cappuccino { Cappuccino() }//動画では=があるが、コンパイルエラーになる
}
func brew<CoffeeType: Coffee>(_: CoffeeType) { }
brew(.cappucino.large)
brew(.regular.tall)
Meet async/await in Swift
async
になるのはメソッドだけとは限らない。プロパティがasync
になることもできる。
extension UIImage {
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 40, height: 40)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}
関連:https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md
Async Sequence
Async await facts
Testing async code
class MockViewModelSpec: XCTestCase {
func testFetchThumbnails() async throws {
XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
}
}
Bridging from Sync to async
続いて、SwiftUIのコードでどのようにasyncなメソッドを取り入れていくかの説明。
既存の同期処理を前提としたクロージャー内でasync
なメソッドを使うためには、Task
で囲う必要がある。
struct ThumbnailView: View {
@ObservedObject var viewModel: ViewModel
var post: Post
@State private var image: UIImage?
var body: some View {
Image(uiImage: self.image ?? placeholder)
.onAppear {
Task {
self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
}
}
}
}
Async APIs in the SDK
既存のSDKのAPIにもasyncなものが追加された。Objective-Cのブロックを引数に取るメソッドもasyncなものに変換される。
デリゲートメソッドも、asyncなものが用意される。
import ClockKit
extension ComplicationController: CLKComplicationDataSource {
func currentTimelineEntry(for complication: CLKComplication) async -> CLKComplicationTimelineEntry? {
let date = Date()
let thumbnail = try? await self.viewModel.fetchThumbnail(for: post.id)
guard let thumbnail = thumbnail else {
return nil
}
let entry = self.createTimelineEntry(for: thumbnail, date: date)
return entry
}
}
引数にとっているcomplicationハンドラーをメソッド内では明示的には実行していないことに注意。
Async alternatives and continuation
既存の完了ハンドラを使ったAPIをasyncなものにラップする
// Existing function
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {
do {
let req = Post.fetchRequest()
req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
completion(result.finalResult ?? [], nil)
}
try self.managedObjectContext.execute(asyncRequest)
} catch {
completion([], error)
}
}
// Async alternative
func persistentPosts() async throws -> [Post] { //非同期で取得する場合、getを落とす
typealias PostContinuation = CheckedContinuation<[Post], Error>
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
// 既存の実装の完了ハンドラ内で、Continuationに非同期処理の結果を渡す
self.getPersistentPosts { posts, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: posts)
}
}
}
}
既存のデリゲートメソッドを使ったAPIをasyncなものでラップする
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
self.activeContinuation = continuation
self.peerManager.syncSharedPosts()
}
}
}
extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
self.activeContinuation = nil // guard against multiple calls to resume
}
func peerManager(_ manager: PeerManager, hadError error: Error) {
self.activeContinuation?.resume(throwing: error)
self.activeContinuation = nil // guard against multiple calls to resume
}
}
Explore structured concurrency in Swift
async let
asyncな関数の結果を待たずに後続の処理を行い、改めて結果が必要になったら待機するために、async let
を使うことができる。
async let result = someAsyncFunc(..)
...
try await result
詳しくは
を参照のこと。
Group tasks
複数の非同期処理を並行に実行するために、Task のGroupを使うことができる。
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
var thumbnails: [String: UIImage] = [:]
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.async {
return (id, try await fetchOneThumbnail(withID: id))
}
}
// Obtain results from the child tasks, sequentially, in order of completion.
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
}
上のコード例で、子タスクの中でthumbnails
に直接代入していないのは、データ競合に関するコンパイルエラーが出るのに対応するため。
Unstructured tasks
Task
オブジェクトを使うことで、既存のUIKitやAppKitのデリゲートメソッド内で非同期処理を行う際に、asyncな関数を呼び出すことができるようになる。
@MainActor
class MyDelegate: UICollectionViewDelegate {
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
Task {
let thumbnails = await fetchThumbnails(for: ids)
display(thumbnails, in: cell)
}
}
}
この場合、Task
内に書かれた非同期処理はメインスレッド内で実行されるものの、非同期処理は完了するまでスレッドをブロックすることはなく、一旦呼び出し元に処理が戻る。
このTask
は、処理が完了するまでは、スコープに縛られずに残り続けることができる。
また、async
でマーキングされていない関数内でも実行することができる。
結果を待ったりキャンセル処理をするには、以下のように処理を書き足す必要がある:
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]//タスクを保持しておくDictionary
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
display(thumbnails, in: cell)
}
}
//セルの表示が終わる際に、表示のための非同期処理のタスクをキャンセルする
func collectionView(_ view: UICollectionView, didEndDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
thumbnailTasks[item]?.cancel()
}
}
Detached tasks
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
Task.detached(priority: .background) {//メインスレッドで実行されるとは限らない
writeToLocalCache(thumbnails)
}
display(thumbnails, in: cell)
}
}
}
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
Task.detached(priority: .background) {
withTaskGroup(of: Void.self) { g in//親タスクがキャンセルされると、このスコープ内の子タスクもキャンセルされる
g.async { writeToLocalCache(thumbnails) }
g.async { log(thumbnails) }
g.async { ... }
}
}
display(thumbnails, in: cell)
}
}
}
Protect mutable state with Swift actors
Syncronize shared mutable state
変わりうる状態を持つオブジェクトを複数のスレッド間で共有することは難しい。
structを使うと、race conditionを防ぐことはできるものの、オブジェクトが複製され、複数の変数は状態が共有されていないため、期待した挙動とは異なってしまう:
struct Counter {
var value = 0
mutating func increment() -> Int {
value = value + 1
return value
}
}
let counter = Counter()
Task.detached {
var counter = counter
print(counter.increment()) // always prints 1
}
Task.detached {
var counter = counter
print(counter.increment()) // always prints 1
}
このように、Race Conditionを防ぎつつShared mutable state の同期が必要な局面がある。
Actorは、shared mutable stateの同期を実現する。
actor isolation
Actorは、プログラムの他の部分から自身の状態を隔離する:
- 自身の状態へのアクセスは、アクターの機能を経由しなければならない
- Actorは、自身の状態へのアクセスはお互いに排他的であることを保証する仕組みを持っている
こうした性質に違反するコードは、コンパイル時の検査でエラーが発生するため、実行できない。
nonisolated
キーワードがついたメソッドは、actorのミュータブルなプロパティにはアクセスできない。
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {//booksOnLoanプロパティにはアクセスできない
hasher.combine(idNumber)
}
}
TaskをPlaygroundで使うとき
import _Concurrency
を入れる必要がある。
Swift concurrency: Update a sample app
func someAsyncFunc() async {...}
// 同期関数の内部でasyncな関数を呼び出す際
func someSyncFunc() {
...
Task { await someAsyncFunc() }
...
}
Xcodeに、既存の関数の非同期版を追加するオプションが用意されている:
DispatchQueue.main.asyncの置き換え
クロージャの手前の DispatchQueue.main.async
をawait MainActor.run
に置き換える。
クロージャ内の変数に関しては、race conditionを防ぐためにキャプチャリストを書くか定数に置き換える必要がある。