🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

Q&A

解決済

1回答

4999閲覧

非同期で繰り返し行っているAPI通信を、繰り返している途中でもキャンセルさせたい

nkrmn_a

総合スコア27

非同期処理

非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

0グッド

1クリップ

投稿2019/12/24 04:38

編集2019/12/24 08:24

前提・実現したいこと

画面①でテーブルセルをタップするとAPI通信で情報を取得し、それを画面②にテーブルで表示させるアプリを作っています。
画面②に遷移すると「戻る」ボタンがあり、画面①に戻れるようになるのですが、APIレスポンスに時間がかかることが想定されているため、API通信中に戻るボタンが押された時はAPI通信を中断させたいです。

API通信は複数回行っており、forEachで引数を変えながら非同期で回っています。

該当のソースコード

swift

1// セルタップしたとき 2func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 3 4 //API通信 5 let queue = DispatchQueue.global(qos: .default) 6 queue.async { 7 self.doMultiAsyncProcess(code: tapCode) 8 } 9} 10 11// 非同期でAPI通信を行う 12func doMultiAsyncProcess(code: String) { 13 14 let dispatchGroup = DispatchGroup() 15 let dispatchQueue = DispatchQueue(label: "queue", attributes: .concurrent) 16 17 // 非同期処理を実行 18 list.forEach { (value) in 19 dispatchGroup.enter() 20 dispatchQueue.async(group: dispatchGroup) { 21 Thread.sleep(forTimeInterval: Double.random(in: 1..<2)) 22 // API通信開始 23 self.getAPIResult(item:value, code:code) 24 dispatchGroup.leave() 25 } 26 } 27}

試したこと

forEachをbreakで抜けようかと思いましたが、出来ないことを知り断念。
forEachをfor-inで書き直し、フラグで管理しながらif文でbreak記述もしましたが、なぜかbreakしてくれず。

本当は非同期の通信を止めるか、メソッドの処理を中断などさせたいのですが、ググっても分からず。
処理のキャンセルはどうやるのが良いのでしょうか。
よろしくお願いします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

オペレーションキューとブロックオペレーションを使って次のようにするのはいかがでしょうか。
(listの情報がないので、一旦Intの配列で実装しています。)

BlockOperationに渡した処理の中で、キャンセル状態を監視してキャンセルされていたら自らやめるようにすれば処理のキャンセルを実現できます。

swift

1 let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2 3 private var operationQueue: OperationQueue? 4 5 /// 複数の処理を並列に実行する 6 func doMultiAsyncProcess(code: String) { 7 guard operationQueue == nil else { return } 8 9 operationQueue = OperationQueue() 10 11 let blockOperation = BlockOperation() 12 let currentQueue = operationQueue 13 blockOperation.completionBlock = { 14 // 全部完了したときの処理。キャンセルした場合も呼ばれる。 15 print("All process for (code) has been done.") 16 // キャンセルされて次の処理が始まっていたら違うキューに変わっている 17 if currentQueue == self.operationQueue { 18 self.operationQueue = nil 19 } 20 } 21 22 // 非同期処理をオペレーションに登録 23 list.forEach { (value) in 24 blockOperation.addExecutionBlock { 25 guard !blockOperation.isCancelled else { return } 26 Thread.sleep(forTimeInterval: Double.random(in: 1..<2)) 27 guard !blockOperation.isCancelled else { return } 28 // API通信開始 29 self.getAPIResult(item:value, code: code, operation: blockOperation) 30 } 31 } 32 33 // 実行開始 34 operationQueue?.addOperation(blockOperation) 35 } 36 37 /// 実行中の処理をキャンセルする。 38 func cancellMultiAsyncProcess() { 39 guard let queue = operationQueue else { return } 40 print("Request cancell of the operations.") 41 queue.cancelAllOperations() 42 operationQueue = nil 43 } 44 45 /// サーバーと通信する。ダミーで単にウェイトして通信した事にする処理としています。また、キャンセルで中断するためにオペレーションも伝えています。 46 func getAPIResult(item: Int, code: String, operation: Operation) { 47 print("getAPIResult(item:(item), code:(code) is started.") 48 Thread.sleep(forTimeInterval: Double.random(in: 1..<5)) 49 if operation.isCancelled { 50 print("getAPIResult(item:(item), code:(code) Cancelled.") 51 } else { 52 print("getAPIResult(item:(item), code:(code) received a result.") 53 } 54 }

多分こんな感じでいいと思いますが、問題あれば教えてください。

なお、元のソースでDispatchGroupへのenter()leave()の呼び出しは必要ありません。

追記

並列度合いを調整するには、オペレーションキューのmaxConcurrentOperationCountを使うと簡単です。
この場合は、キューが処理数を制御できるように、ひとつひとつの処理をそれぞれ別のBlockOperationに分割して登録することになります。また、こうすると全処理の完了検知にBlockOperationcompletionを使うことができないので、かわりに、completionの処理を一つのBlockOperationとして、他の処理が終わったら実行するように設定してオペレーションキューに入れました。
このように変更したdoMultiAsyncProcessは以下のようになると思います。

func doMultiAsyncProcess(code: String) { guard operationQueue == nil else { return } operationQueue = OperationQueue() // 並行処理数を3つまでに制限。 operationQueue?.maxConcurrentOperationCount = 3 // 全部が完了したら実行する処理。(キャンセルした時は呼ばれない) let currentQueue = operationQueue let completion = BlockOperation() completion.addExecutionBlock { print("All process for (code) has been done.") if currentQueue == self.operationQueue { self.operationQueue = nil } } // 非同期処理をオペレーションキューに登録 list.forEach { (value) in var blockOperation = BlockOperation() blockOperation.addExecutionBlock { guard !blockOperation.isCancelled else { return } Thread.sleep(forTimeInterval: Double.random(in: 1..<2)) guard !blockOperation.isCancelled else { return } // API通信開始 self.getAPIResult(item:value, code: code, operation: blockOperation) } completion.addDependency(blockOperation) operationQueue?.addOperation(blockOperation) } operationQueue?.addOperation(completion) }

投稿2019/12/24 07:57

編集2019/12/28 08:44
eytyet

総合スコア803

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nkrmn_a

2019/12/25 03:46 編集

確かに全部の通信が同時に発生しています。 DispatchGroupは、メソッドの制御をしてほしいためです。 叩くサーバの関係上、直列にしてしまうとレスポンスが大変遅くなってしまうため並列にしております。 メソッドが実行済みなのは承知しているのですが、実行中にキャンセルする方法などはないのでしょうか…
eytyet

2019/12/24 08:48

ForEachが済んでいるのを理解しつつ、breakでやめられると思ったという事ですか? キャンセルという言葉で、具体的に何をされたいのでしょうか。通信をとめるなら、通信プロトコル上の処理の話になります。受け取った結果を処理するのをやめるなら、自分で処理しないように書くとよいです。 DispatchQueueにはキャンセル機構はありませんが、OperationQueueならキャンセルもできます。そういう意味のご質問でしょうか。 DispatchGroupについては、どのように使えているのか、私にはちゃんと理解できていないようです。すいません。よくみたら、sleep時間がランダムなので、通信順序は不定なのですね。DispatchGroupに、enter順に実行するような機能があるのでしょうか?
nkrmn_a

2019/12/25 03:49

すみません、私自身どの方法でやるのが正解なのかわかっていなく、迷走した状態での質問だったので混乱させてしまいました。 処理のキャンセルというのは、私は通信であれ受け取った結果の処理であれ、メソッドの内部で動いている処理を、という風に思っていました。今書いている記述だと通信と結果の処理を同一メソッド内で行っているので、そのメソッド自体を止める方法はあるのかなと思い質問した次第です。 OperationQueueのキャンセルも調べましたが、どのような場合に向いているのか、どういう動き方をしているのか理解できなかったため、他の方法を考えていました。 OperationQueueはどのようなものなのでしょうか。この場合OperationQueueを使うのは向いているのでしょうか…。 DispatchGroupは以下のサイトを参考にしました。簡単にいうと非同期処理が全て終わった検知をしてくれています。全てのleaveが実行された後に、全非同期完了後にしたい処理を実行できます。また、sleep時間はランダムですが、通信順序というよりは通信開始のタイミングが不定です。 https://qiita.com/shtnkgm/items/d9b78365a12b08d5bde1
nkrmn_a

2019/12/26 04:18

ありがとうございます!キャンセルできるようになりました。 それと重ねて質問失礼します。。(もし新しく質問立てた方がいい件でしたらそういたします) forEachで回したblockOperationをoperationQueueに一気にaddして実行していると思います。 operationQueueでlistに入っている数ぶん一気に通信が回る(と解釈しています)ので、結構負荷が大きく動作が遅くなってしまいました。 負荷を減らすことができる方法をご存知でしたら教えていただきたいです。 負荷がかかり過ぎたらアプリが落ちる可能性などはあり得ますでしょうか…?
eytyet

2019/12/26 06:38 編集

メモリが足りなくなれば落ちると思います。 動作が遅くなってしまった、というのは、メインスレッドの動作が邪魔されるほどサブスレッドが重いということでしょうか。並列の度合いを制御したいということでしたら、OperationQueueのmaxConcurrentOperationCountを使って並列の個数を制限できます。それを使うためにブロックオペレーションを複数に分割して、completionをそれら全部にaddDependencyして一つのオペレーションとして作成して実行するという手もあるかと思います。(この場合は、キャンセル時にはcompletionが呼ばれません。)
nkrmn_a

2019/12/27 00:59

並列の度合いを制御したいということです。 "completionをそれら全部にaddDependencyして"という部分がよく分かっていないのですが、複数のブロックオペレーションそれぞれ一つずつにcompletionをaddDependencyするという解釈で正しいですか?
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問