🧵

ゆる〜く学ぶSwiftのConcurrency ~前半~

2021/12/01に公開

この記事はSwift/Kotlin愛好会 Advent Calendar 2021 1日目の記事です!

この記事はSwift.orgのConcurrencyをゆる〜く日本語で読み下したものです。これからConcurrency学ぶよって方はこの記事でざっくり内容を掴んでからぜひ原文も読んでみてください〜

後半はこちら

以下本文

前置き

Swiftには非同期(Asynchronous)、並列(parallel)なコードの書き方がちゃんとサポートされてるよ!

非同期コードは1度に1個しか実行されないけど一時停止と再開ができるんだ。そのおかげでネットワーク経由でのデータフェッチとかファイルの解析みたいな長い時間かかる処理をしながらUI更新のような素早い処理を続けることができるんだよ。

並列コードは複数のコードを同時に実行できるってことだ。例えば4コアのCPUだったら4つのコードを同時実行できるんだよ。

普通、非同期/並列コードを書こうとするとめっちゃ複雑になるんだけど Swiftはコンパイル時のチェックが使えたりするよ。例えばActorってのを使うんだ。まぁ普段より気をつけないといけないのは変わらないけどSwiftが言語レベルで並行性(concurrency)をサポートしてくれてるからコンパイル時に気づけて便利だね!

以下では concurrency という言葉を非同期と並列の組み合わせとして使うね。

なお、Swiftの言語サポートがなくても並行(concurrent)なコードを書くことはできたけどクロージャーのネストが続いたりして読みづらいよね。

listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}

非同期関数の定義と呼び出し

非同期関数とは実行の途中で中断できる特殊な関数だよ。

非同期関数は async キーワードを関数定義に追加するよ。 throwsと同じようなポジションに書くよ。throwsasyncを両方使うときはasync throwsの順で書くよ。

非同期関数を呼び出すとその関数が戻ってくるまで実行は一時停止するよ。一時停止する可能性のあるポイントにはawaitをつけるよ。

例えばこんな感じ

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

awaitがついてるlistPhotos(inGallery:)とdownloadPhoto(named:)で処理が戻ってくるまで一時停止するよ。

awaitがついてるコードは実行を一時停止する必要があるので特定の場所のみで呼び出すことができるよ。

  • 非同期の関数、メソッド、プロパティ
  • @mainがついたstruct, class, enumのstatic main()メソッド
  • 分離された子タスクのコード(後述)

Asynchronous Sequences

非同期関数で配列の要素を取得するような場合に、配列のすべての要素が揃ってから配列全体を一度に返す方法もあるけどasynchronous sequence(非同期シーケンス)を使って1個1個待つことも可能だよ。

こんな感じ!

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

次の要素を取り出すときに一時停止の可能性がありですね。

独自の型をAsyncSequenceプロトコルに適合させるとfor-await-inで使えるようになるよ。

非同期関数を並列に呼び出す

await付きで非同期関数を呼び出すと一度に1つのコードしか実行できないんだ。非同期関数が戻ってきてから次の処理を実行するので、例えば次のコードは3回downloadPhoto(named:)を待つことになる。これは順次実行させたいときにいいね。

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

async letを利用すると並列に非同期関数を実行できるよ。こうするとdownloadPhoto(named:)はそれぞれ並列に実行されるけど、awaitの部分で3つのdownloadPhoto(named:)の結果が揃うまで待つようになるよ。

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

順次と並列を組み合わせてうまく使っていこな!

後半に続く

Discussion