148
88

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DispatchQueue→Task置き換えまとめ(Swift)

Last updated at Posted at 2022-09-28

はじめに

Objective-C時代から私たちを支えてくれたDispatchQueueですが、Swift Concurrency(async/await)の登場によりその役目を終えようとしています。

本記事では DispatchQueue を使った様々なユースケースを Task に置き換える方法を紹介します。

注意

  • 基本的にiOS 13.0+では DispatchQueue を使う必要がない
    • DispatchQueue でしか実現できない処理があったら教えてほしい
      • DispatchQueue はキューに名前を付けられるので、デバッグしやすいというメリットはある
  • Before/Afterで処理が完全に同じになるかまでは確認していない

環境

  • OS:macOS Monterey 12.5.1
  • Xcode:14.0 (14A309)
  • Swift:5.7

置き換え一覧

DispatchQueueTask 置き換えの一覧です。

DispatchQueue Task
DispatchQueue.main.sync(excute:) MainActor.run(resultType:body:)
DispatchQueue.main.async(excute:) Task.detached { @MainActor in ... }
DispatchQueue.async(execute:) (シリアルキュー) actor
DispatchGroupDispatchQueue.async(group:execute:) async let
DispatchGroup.enter()DispatchGroup.leave() withCheckedThrowingContinuation(function:_:) または withCheckedContinuation(function:_:)
DispatchQueue.main.asyncAfter(deadline:execute:) iOS 13.0+: Task.sleep(nanoseconds:)
iOS 16.0+: Task.sleep(for:)

置き換え方法は他にもあると思います。ここでは書きやすい方法を紹介しています。

ユースケース

置き換えをユースケースごとに紹介します。

特定の処理が完了するのを待つ

特定の処理が完了するのを待つユースケースです。

今までは DispatchQueue.main.sync(excute:) を使っていましたが、これからは MainActor.run(resultType:body:) を使います。

以下のサンプルでは、BeforeもAfterも 12 の順番で出力されます。

// Before
DispatchQueue.main.sync {
  print("1")
}
print("2")

// After
Task {
  await MainActor.run {
    print("1")
  }
  print("2")
}

もっとも DispatchQueue.main.sync(excute:) はメインスレッドで実行するとデッドロックのためにクラッシュしたりと、基本的には使わないはずなので、このユースケースの置き換えはあまりないかもしれません。

特定の処理を非同期で実行する

特定の処理を非同期で実行するユースケースです。

今までは DispatchQueue.main.async(excute:) を使っていましたが、これからは Task.detached { @MainActor in ... } を使います。

この置き換えはSwiftのプロポーザルに書いてありました。

以下のサンプルでは、BeforeもAfterも 21 の順番で出力されます。

// Before
DispatchQueue.main.async {
  print("1")
}
print("2")

// After
Task.detached { @MainActor in
  print("1")
}
print("2")

タスクを切り離す必要がないときは、 Task { @MainActor in ... } と書けます。
クロージャ内の処理をメインスレッドで実行したいだけの場合、 Task.detached を使わないほうが多いと思います。

// このように書くことのほうが多い
- Task.detached { @MainActor in
+ Task { @MainActor in
  print("1")
}
print("2")

データ競合を防ぐ

データ競合を防ぐユースケースです。

今までは DispatchQueue でシリアルキューを作って実現していました。
これからは actor を使って実現します。

// Before
final class Counter {
  private let queue = DispatchQueue(label: "com.example.myqueue") // シリアルキュー

  private var count = 0

  func increment() {
    queue.async { [self] in
      count += 1
    }
  }

  func decrement() {
    queue.async { [self] in
      count -= 1
    }
  }
}

// After
actor Counter {
  private var count = 0

  func increment() {
    count += 1
  }

  func decrement() {
    count -= 1
  }
}

かなりスッキリしました。

詳細は以下の記事をご参照ください。

複数の処理を非同期で実行し、完了するのを待つ

複数の処理を非同期で実行し、完了するのを待つユースケースです。

今までは DispatchQueue.async(group:execute:) を使っていましたが、これからは async let を使います。
非同期メソッドの戻り値がなくても、 async let で明示的に Void を指定することで変数として代入できます。

// Before
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.example.myqueue1")
let queue2 = DispatchQueue(label: "com.example.myqueue2")
let queue3 = DispatchQueue(label: "com.example.myqueue3")

queue1.async(group: group) {
  print("1")
}
queue2.async(group: group) {
  print("2")
}
queue3.async(group: group) {
  print("3")
}

group.notify(queue: .main) {
  print("4")
}

// After
async let queue1: Void = printAsync("1")
async let queue2: Void = printAsync("2")
async let queue3: Void = printAsync("3")

_ = await (queue1, queue2, queue3)

print("4")

private func printAsync(_ text: String) async {
  print(text)
}

クロージャの実行を待つ

クロージャの実行を待つユースケースです。

今まではクロージャへ入る前に DispatchGroup.enter() を実行し、クロージャを抜けるときに DispatchGroup.leave() を実行して実現していました。
これからは withCheckedThrowingContinuation(function:_:) (または withCheckedContinuation(function:_:) )を使い、非同期メソッドに変換して await することで実現します。

以下のサンプルでは、BeforeもAfterも 12 の順番で出力されます。

let request = URLRequest(url: URL(string: "https://example.com")!)

// Before
let group = DispatchGroup()

group.enter()
URLSession.shared.dataTask(with: request) { data, response, error in
  if let error { return }
  guard let data, let response else { return }

  print("1")

  group.leave()
}.resume()

group.notify(queue: .global()) {
  print("2")
}

// After
_ = try! await URLSession.shared.data(from: request)

print("2")

private extension URLSession {
  func data(from request: URLRequest) async throws -> (Data, URLResponse) {
    struct BadServerResponseError: Error {}
    return try await withCheckedThrowingContinuation { continuation in
      self.dataTask(with: request) { data, response, error in
        if let error {
          return continuation.resume(throwing: error)
        }
        guard let data, let response else {
          return continuation.resume(throwing: BadServerResponseError())
        }

        print("1")

        continuation.resume(returning: (data, response))
      }.resume()
    }
  }
}

スローしない場合は withCheckedThrowingContinuation(function:_:) の代わりに withCheckedContinuation(function:_:) を使います。

- func data(from request: URLRequest) async throws -> (Data, URLResponse) {
+ func data(from request: URLRequest) async -> (Data?, URLResponse?) {
-   struct BadServerResponseError: Error {}
-   return try await withCheckedThrowingContinuation { continuation in
+   await withCheckedContinuation { continuation in
      self.dataTask(with: request) { data, response, error in
-       if let error {
+       if error != nil {
-         return continuation.resume(throwing: error)
+         return continuation.resume(returning: (nil, nil))
        }
-       guard let data, let response else {
-         return continuation.resume(throwing: BadServerResponseError())
-       }

        print("1")

        continuation.resume(returning: (data, response))
      }.resume()
    }
  }
}

戻り値がない場合は continuation.resume(returning:) の代わりに continuation.resume() を使います。

- func data(from request: URLRequest) async throws -> (Data, URLResponse) {
+ func data(from request: URLRequest) async throws {
    // ...
    return try await withCheckedThrowingContinuation { continuation in
      self.dataTask(with: request) { data, response, error in
        // ...
-       guard let data, let response else {
+       guard data != nil, response != nil else {
          return continuation.resume(throwing: BadServerResponseError())
        }
        // ...
-       continuation.resume(returning: (data, response))
+       continuation.resume()
      }.resume()
    }
  }

コンプリションハンドラを非同期メソッドへ変換するのは最終手段であり、もし標準で非同期メソッドが用意されているならそちらを使うべきです。
例えば data(from:delegate:) はiOS 15.0+から使えるので、iOS 14以下で非同期メソッドにしたい場合のみ変換するのが望ましいです。

特定の処理を遅延実行する

特定の処理を遅延実行するユースケースです。

今までは DispatchQueue.main.asyncAfter(deadline:execute:) を使い、クロージャとして実行していました。
これからは await Task.sleep(nanoseconds:) を使い、スリープさせることで遅延実行を表現します。

ただ指定する単位がナノ秒と、少し使いづらいです。
Xcode 14.1(Swift 5.7.1)かつiOS 16.0+では Task.sleep(for:) を使って単位を指定できるので、こちらを使うのが望ましいです。

// Before
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
  // ...
}

// After
Task {
  // iOS 13.0+
  try await Task.sleep(nanoseconds: 100_000_000)

  // iOS 16.0+
  try await Task.sleep(for: .nanoseconds(100_000_000))
  try await Task.sleep(for: .microseconds(100_000))
  try await Task.sleep(for: .milliseconds(100))
  try await Task.sleep(for: .seconds(0.1))
  // ...
}

置き換えないケース

例外として、置き換えずに DispatchQueue を使い続けるケースを紹介します。

Combineでスケジューラを指定して値を受け取る

Combineでスケジューラを指定して値を受け取るケースです。

Publisher.sink(receiveValue:) 内でゴニョゴニョしても同じことが実現できます。
しかし Publisher.receive(on:options:)DispatchQueue.main を指定するほうがスマートなので、この場合は DispatchQueue を使ってもいいと考えます。

// ✅Before
pub.receive(on: DispatchQueue.main).sink { [weak self] _ in
  // ...
}

// ⚠After
pub.sink { [weak self] _ in
  Task.detached { @MainActor in
    // ...
  }
}

おわりに

これで DispatchQueueTask に置き換えられ、スマートになりました :relaxed:

DispatchQueue を使ったユースケースは他にもあるので、コメントを頂いたりしたら追記したいと思います。

参考リンク

148
88
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
148
88

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?