swift で直列に非同期処理

swift

swift で非同期処理を扱うときは GCD (Grand Central Dispatch) を使います。

以下に詳しくかかれていますが、GCD のキューに非同期処理を突っ込むんですね。

あとは GCD がよしなになってくれます。

で、そのキューは、直列型と並列型があります。

並列型ってのは、みんな同時に一斉に処理を始めて、終わるのは各自バラバラのタイミング。

直列型ってのは、順番に処理を始めて、1つの処理が終わったら次の処理にうつる。

えっ、直列型って、普通の処理と何が違うの?非同期処理じゃなくて同期処理じゃん。

と思った方。正しいです。

この直列型、というのは、非同期でしか描けない処理(通信とかsleepとか)を無理やり順番に実行させるために使います。

そうしないと、各自バラバラに同時に動いてしまうんですね。

Swiftで複数の非同期処理の完了時に処理を行う – 2021年05月23日
https://qiita.com/shtnkgm/items/d9b78365a12b08d5bde1

で、試しに上記サイトの直列のサンプルを実行していみました。以下です。

func doMultiAsyncProcess() {
    let dispatchGroup = DispatchGroup()
    // 直列キュー / attibutes指定なし
    let dispatchQueue = DispatchQueue(label: "queue")

    // 5つの非同期処理を実行    
    for i in 1...5 {
        dispatchQueue.async(group: dispatchGroup) {
            [weak self] in
            self?.asyncProcess(number: i) {
                (number: Int) -> Void in
                print("#\(number) End")
            }
        }
    }

    // 全ての非同期処理完了後にメインスレッドで処理
    dispatchGroup.notify(queue: .main) {
        print("All Process Done!")
    }
}

// 非同期処理
func asyncProcess(number: Int, completion: @escaping (_ number: Int) -> Void) {
    print("#\(number) Start")
    let interval  = TimeInterval(arc4random() % 100 + 1) / 100
    DispatchQueue.global().asyncAfter(deadline: .now() + interval) {
        completion(number)
    }
}

その実行結果が以下です。

#1 Start
#2 Start
#3 Start
#4 Start
#5 Start
All Process Done!
#2 End
#3 End
#5 End
#4 End
#1 End

あれ?書かれてる結果と違いますね。

全部のEnd が終わる前に All Process Done! が来てしまっています。

dispatchGroup

GCD(Grand Central Dispatch)のDispatchGroupを作成し、
非同期処理の実行前にenter()、実行後にleave()を呼ぶことで、複数の非同期処理の開始と完了を管理します。
全ての処理で完了の合図としてleave()が呼ばれた後に、notify()メソッドで指定したクロージャが実行されます。

dispatchGroup.enter() と dispatchGroup.leave() を使ってみます。

func doMultiAsyncProcess() {
    let dispatchGroup = DispatchGroup()
    // 直列キュー / attibutes指定なし
    let dispatchQueue = DispatchQueue(label: "queue")

    // 5つの非同期処理を実行
    for i in 1...5 {
        dispatchQueue.async(group: dispatchGroup) {
            [weak self] in
            dispatchGroup.enter()
            
            self?.asyncProcess(number: i) {
                (number: Int) -> Void in
                print("#\(number) End")
                
                dispatchGroup.leave()
            }
        }
    }

    // 全ての非同期処理完了後にメインスレッドで処理
    dispatchGroup.notify(queue: .main) {
        print("All Process Done!")
    }
}

// 非同期処理
func asyncProcess(number: Int, completion: @escaping (_ number: Int) -> Void) {
    print("#\(number) Start")
    let interval  = TimeInterval(arc4random() % 100 + 1) / 100
    DispatchQueue.global().asyncAfter(deadline: .now() + interval) {
        completion(number)
    }
}

そしたら、全部のEnd が終わってから All Process Done! が来るようになりました。

#1 Start
#2 Start
#3 Start
#4 Start
#5 Start
#2 End
#4 End
#5 End
#1 End
#3 End
All Process Done!

しかし、まだ完全に直列ではありません。

DispatchSemaphore

DispatchSemaphore – Apple Developer
https://developer.apple.com/documentation/dispatch/dispatchsemaphore

なんらかの大人の事情で、非同期処理を同期的なメソッドと同じような書き方で処理したい場合があると思います。 (もちろん良いとは言えないですが) 例えば、completion handlerで順番に処理したいことが多くなり、JSのcallback hellのように、ネストが深くなってしまう場合です。

そのような場合に、DispatchSemaphoreを活用することで、処理の記述を見やすくすることができます。

実装は至ってシンプルで、DispatchSemaphoreは内部でカウントを持ち、waitでデクリメントをして、
カウントが0以上になるまでブロックします。
signalでインクリメントをするので、処理が完了した際にsignalを呼ぶという使い方になります。

DispatchSemaphoreで非同期処理の完了を待つ
https://scior.hatenablog.com/entry/2019/09/11/231626

dispatchSemaphore.signal() と dispatchSemaphore.wait() を使います。

コードは以下のようになります。

func doMultiAsyncProcess() {
    let dispatchGroup = DispatchGroup()
    // 直列キュー / attibutes指定なし
    let dispatchQueue = DispatchQueue(label: "queue")

    // 5つの非同期処理を実行
    for i in 1...5 {
        let dispatchSemaphore = DispatchSemaphore(value: 0)
        
        dispatchQueue.async(group: dispatchGroup) {
            [weak self] in
            dispatchGroup.enter()
            
            self?.asyncProcess(number: i) {
                (number: Int) -> Void in
                print("#\(number) End")
                
                dispatchGroup.leave()
                dispatchSemaphore.signal()
            }
            
            dispatchSemaphore.wait()
        }
    }

    // 全ての非同期処理完了後にメインスレッドで処理
    dispatchGroup.notify(queue: .main) {
        print("All Process Done!")
    }
}

// 非同期処理
func asyncProcess(number: Int, completion: @escaping (_ number: Int) -> Void) {
    print("#\(number) Start")
    let interval  = TimeInterval(arc4random() % 100 + 1) / 100
    DispatchQueue.global().asyncAfter(deadline: .now() + interval) {
        completion(number)
    }
}

以下のように、Start と End が終わってから次の Start に入るようになりました。

期待通りの結果が得られましたね。

#1 Start
#1 End
#2 Start
#2 End
#3 Start
#3 End
#4 Start
#4 End
#5 Start
#5 End
All Process Done!

めでたしめでたし

タイトルとURLをコピーしました