Async 、 Task 、そして MainActor

async/await使ってみましたか? コードがすっきり書けるようになって、とても便利ですよね。

必要な情報だけチェックしてすぐ動かして確認したい方向けに、ざっくりした情報を書いてみました。

すぐ使えるSwift Concurrency!なので、とりあえず書いてみてください。

ざっくりasync

async/awaitは、Swiftに導入された新しい非同期処理です。

多くのAPIがasync/awaitに対応していますが、動作確認で一番手軽なのはURLSessionですね。

今までのコードだとこんな感じですが……

        let url = URL(string: "https://www.apple.com/")!

        URLSession.shared.dataTask(with: url, completionHandler: {data, respose, error in
            
            // エラー処理

            let decoder = JSONDecoder()
            if let data = data{
                let receivedData = try! decoder.decode(MyClass.self, from: data)
            }
            
            // 画面処理など
            
        }).resume()

async/awaitでかくとこうなります。

        let url = URL(string: "https://www.apple.com/")!
        let (data, response) = try! await URLSession.shared.data(from: url)
        let receivedData = try! decoder.decode(MyClass.self, from: data)
        // エラー処理
        // 画面処理など

ここでは、URLSessionのdata(from url: URL)がasyncに対応したAPIになっています。

asyncなメソッドをよぶときには、awaitを記述します。

非常にすっきりと、直感的な記述ができますね。

asyncをよべる場所は限られる

実は、asyncなコードをどこからでもよべるわけではありません。

例えば、UIViewControllerのviewDidLoadなどでasyncなメソッドをよぼうとすると、「'async' call in a function that does not support concurrency」とwarningが表示されてコンパイルに失敗します。

asyncなコードをよべるのは下記の3箇所だけです。

  • 非同期な関数やメソッドの中
  • mainメソッドや@mainがつけられた場所
  • detachedTaskやunstructured Taskの中

この unstructured TaskとdetachedTaskがどんなものなのかみてみましょう。

unstructured Taskと detached Task

どちらのTaskも、優先度をつけて生成することもできますし、デフォルトの優先度で生成することができます。

  • unstructured Task
// デフォルトの優先度の場合
Task{
  // ここでasyncなメソッドを呼ぶ
}

// 優先度backgroundの場合
Task(priority: .background){
  // ここでasyncなメソッドを呼ぶ
}
  • detached Task
// デフォルトの優先度の場合
Task.detached(){
  // ここでasyncなメソッドを呼ぶ
}

// 優先度userInitiatedの場合
Task.detached(priority: .userInitiated) {
  // ここでasyncなメソッドを呼ぶ
}

Taskの優先度は .high、.middle、.backgroundなどありますが、処理の優先度に応じて選びましょう。

unstructured Taskと detached Taskの違い。

さて、この二つの種類のTaskは何が違うのでしょうか。

unstructured Taskは実行コンテキストを引き継ぎますが、detached Taskは実行コンテキストを引き継ぎません。

わかりやすくいうと、この二つのTaskは開始時の実行スレッドが違います。

unstructured Taskは、そのTaskがよばれた場所の実行スレッドをそのまま引き継ぎ、detached Taskは、引き継ぎません。

unstructured Taskは、メインスレッドからよばれればメインスレッドで実行され、バックグラウンドスレッドでよばれればバックグラウンドスレッドで実行されます。 それに対して、detached Taskはどのスレッドからよばれても、バックグラウンドスレッドよばれたスレッド以外のスレッドで実行されます。

例えばViewControllerのviewDidLoadでTaskをよんだとき、どんなスレッドでよばれるかをみてみましょう。

(Thread.isMainThreadを使うと、その行がメインスレッドで実行されているかどうかがわかります。)

class ViewController: UIViewController {
...
    override func viewDidLoad() {
        super.viewDidLoad()

        print("isMainThread:\(Thread.isMainThread)")  // これはtrueになります。

        Task{
            print("isMainThread:\(Thread.isMainThread)") // これはtrueになります。
        }

        Task(priority: .background){
            print("isMainThread:\(Thread.isMainThread)") // これはtrueになります。
        }

        Task.detached(){
            print("isMainThread:\(Thread.isMainThread)") // これはfalseになります。
        }

        Task.detached(priority: .userInitiated) {
            print("isMainThread:\(Thread.isMainThread)") // これはfalseになります。
        }
    }
}

UIViewControllerのviewDidLoadはメインスレッドでよばれるので、そこで作られたunstructured Taskはメインスレッドでよばれ、detached Taskはメインスレッドではよばれないということです。

こんな感じでunstructured Taskとdetached Taskを組み合わせるとどうなるでしょうか。(自分で確認してみましょう。)

        Task(priority: .background){
            Task.detached(priority: .userInitiated){
                print("isMainThread:\(Thread.isMainThread)")
            }
        }

        Task.detached(priority: .userInitiated){
            Task(priority: .background){
                print("isMainThread:\(Thread.isMainThread)")
            }
        }

(2021/12/5 追記)なお、Task{}のなかがずっと同じスレッドで実行されるわけではなく、awaitをはさんだタイミングで実行スレッドがかわる場合もあります。

メインスレッドですべき処理、するべきではない処理。

さて、ここで初心者の方むけに、メインスレッドですべき処理、するべきではない処理を復習しましょう。

基本的に、画面描画に関わる処理はメインスレッドで行わなければいけません。画面に表示する文字列を変更したり、新しい画面を開いたり、画面に表示された画像を変更したり……あらゆる処理はメインスレッドで行いましょう。

また、時間のかかる処理をメインスレッドで行ってしまうと、画面の動きがもたついたり、ユーザーに対するレスポンスが遅くなってしまうので、バックグラウンドのスレッドで行う必要があります。

たとえば、URLSessionで取得したjsonデータをdecodeし、なんらかの画面表示を行う場合には、

  • Networkよびだし -> バックグラウンドのスレッドで行う
  • エラー処理(画面表示には関係ない部分)-> バックグラウンドのスレッドで行う
  • エラー処理(エラー画面表示など、画面表示をする部分)-> メインスレッドで行う
  • json decode-> バックグラウンドのスレッドで行う
  • json decode の結果を画面に表示する-> メインスレッドで行う
  • json decode の結果から、画面表示とは関係のない処理を行う-> バックグラウンドのスレッドで行う

が望ましいです。

ただ、あまり細かく実行スレッドを指定すると、スレッド切り替えの際にバグが発生してしまうので注意が必要です。

MainActor

そこで登場するのがMainActorです。

iOS13から登場したActorを使うと、どこからよばれてもメインスレッドで実行するように指定することができます。

指定方法はいろいろありますが、ここでは簡単にクラス単位の指定とメソッド単位の指定をみてみましょう。

クラス単位で@MainActorをつけておくと、そのクラスのすべてのメソッドがメインスレッドでよばれるようになります。 (asyncなメソッドは別です)

@MainActor
class IsMainActor{
    
    func hello(){
        print("Thread.isMainThread:\(Thread.isMainThread)") // どこからよばれてもtrue
    }

}

また、クラスの一部の処理だけメインスレッドで実行したい場合には、メソッドに@MainActorをつけると対応できます。

class IsNotMainActor{
    func hello(){   
        print("Thread.isMainThread:\(Thread.isMainThread)") // こちらはよばれた場所によります。
    }
    
    @MainActor
    func helloMain(){
        print("Thread.isMainThread:\(Thread.isMainThread)") // どこからよばれてもtrue

    }
}

ちなみに、UIViewControllerは@MainActorがついているので、普通のメソッド(async以外)は常にメインスレッドでよばれます。 UIViewControllerのメソッドには@MainActorをつけなくても、常にメインスレッドでよばれます。

ですので、UI更新系の処理には必ず@MainActorをつけておくことで、安全なコードを書けますね。

まとめ

こんな感じで書くと、ネットワーク処理やjson decodeはバックグラウンドスレッドでよばれ、UIを含む処理はメインスレッドでよばれるようになります。

class ViewController: UIViewController {
...
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task.detached(){

            let url = URL(string: "http://www.mysite.com/")!
            do{

                let (data, response) = try await URLSession.shared.data(from: url)
                
                let decoder = JSONDecoder()
                let decodedData = try decoder.decode(MySuperClass.self, from: data)
                
                // UI変更を含む処理(メインスレッドで実行される)
                await IsMainActor().hello()
                await IsNotMainActor().helloMain()
                
                // UI変更を含まない処理(メインスレッド以外で実行される)
                await IsNotMainActor().hello()
            }
            catch{
                await self.showError()
            }  
        }
    }
}

ただ、Appleのサンプルなどをみていても、上記のようなコードでTask.detached(){}ではなくてTask{}を使っていたりもするので、それほど使い分けに注意しなくてもいいかもしれないですね。

(長くなるので端折りますが、サスペンションポイントがあるのでDispatchQueue.mainより安全ですし……。)

(2021/12/5 追記) また、この記事をかいたあとに、tokoromさんと話して、AppleのConcurrency関連の資料を見ていると、Task.datached{} より Task{} のほうが推奨されているような印象があるよね……という結論になったので、上ではこう書きましたが、個人的にはTask{}を使おうかなと思います。