Tana Gone
Tana Gone
2 min read

Categories

非同期メソッドを同期コンテキストで呼ぶ

SQLite3のDBからデータを取り出す処理が非同期アクセスである場合、Taskオブジェクトで包んやれば同期コンテキストで呼び出す事ができる。しかし、Taskオブジェクトの生成は非同期に行われてしまう。非同期アクセスの結果を待ってクラスのプロパティーを初期化するコードをネットを漁って見つけた。

  • 次の2つのTextにはWinStateクラスのcodes配列の要素数と配列の要素が表示されている。

    TaskSync

  • WinStateクラスは次のとおりである。NWerフレームワークはNetworkerオブジェクトを提供していて、そこにはqueryCodeTblメソッドが生えている。CodeTblテーブルへselect queryを投げて文字列を要素とする配列の配列が返ってくる。このqueryCodeTblメソッドはTask.syncメソッドで包んで実行すれば同期コンテキストで呼び出せる。Task.syncメソッドのコードは次のセクションにあるとおりである。

import SwiftUI
import NWer

class WinState: ObservableObject {
  @Published var codes: [[String]] = []
  init() {
    do {
      codes = try Task.sync {
        try await Networker.queryCodeTbl(
          DBPath.dbPath(1), DBPath.dbPath(2))
      }
    } catch {
      print("error: \(error)")
    }
  }
}
  • Task.syncのコード、これがネットを漁ってヒットしたものである。withoutActuallyEscapingグローバル関数を使っており一見何をやっているのか良くわからん。別途解読する事にする。

    import Dispatch
      
    extension Task {
      /// Executes the given async closure synchronously, waiting for it to finish before returning.
      ///
      /// **Warning**: Do not call this from a thread used by Swift Concurrency (e.g. an actor, including global actors like MainActor) if the closure - or anything it calls transitively via `await` - might be bound to that same isolation context.  Doing so may result in deadlock.
      static func sync(_ code: sending () async throws(Failure) -> Success) throws(Failure) -> Success { // 1
        let semaphore = DispatchSemaphore(value: 0)
      
        nonisolated(unsafe) var result: Result<Success, Failure>? = nil // 2
      
        withoutActuallyEscaping(code) { // 3
          nonisolated(unsafe) let sendableCode = $0 // 4
      
          let coreTask = Task<Void, Never>.detached(priority: .userInitiated) { @Sendable () async -> Void in // 5
            do {
              result = .success(try await sendableCode())
            } catch {
              result = .failure(error as! Failure)
            }
          }
      
          Task<Void, Never>.detached(priority: .userInitiated) { // 6
            await coreTask.value
            semaphore.signal()
          }
      
          semaphore.wait()
        }
      
        return try result!.get() // 7
      }
    }
      
    

    Calling Swift Concurrency async code synchronously in Swift – Wade Tregaskis

    • なお、WinStateを注入するApp構造体、注入されたContentViewのコードは次のとおり
@main
struct QueryHistApp: App {
  var a: WinState = .init()
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(a)
        .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 300, idealHeight: 300, maxHeight: 300)
    }
  }
}
  • ContentViewは次のとおり
struct ContentView: View {
  @EnvironmentObject private var env: WinState
  @State private var index: Int = 0
  @State private var txt: String = "0"

  var body: some View {
    VStack {
      Text("Codes: \(env.codes.count)")
        .font(.largeTitle)
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)

      Text("Codes: \(env.codes[index])")
        .font(.headline)
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
      HStack {
        Spacer(minLength: 100)
        TextField("Index", text: $txt).font(.largeTitle)
          .onSubmit {
            index = Int(txt) ?? 0
            print("Index: \(index)")
          }
        Spacer(minLength: 100)
      }.padding(.top, 10)

    }
  } // body
}
  • 非同期処理を同期コンテキストとして呼び出すには別解があり、それが次のコードである。

      init() {
        do {
          codes = try async {
            try await Networker.queryCodeTbl(
              DBPath.dbPath(1), DBPath.dbPath(2))
          }
        } catch {
          print("error: \(error)")
        }
      }
    
  • グローバル関数async関数のコード semaphoreの生成, semaphore.wait()の間がクリティカル・セクションで非同期アクセスの完了を待つ部分である。

    import class Dispatch.DispatchSemaphore
      
    func async<Success>(await body: () async throws -> Success) rethrows -> Success {
      try withoutActuallyEscaping(body) { body in
        var result: Result<Success, Error>!
        withoutActuallyEscaping({ result = $0 }) { setter in
          let semaphore = DispatchSemaphore(value: 0)
          Task<Void, Never> {
            defer {
              semaphore.signal()
            }
            do {
              try await setter(.success(body()))
            } catch {
              setter(.failure(error))
            }
          }
          semaphore.wait()
        }
        return try result.get()
      }
    }
    

    Concurrencyの応答を非同期でないコンテキストで

  • sleepを使っても非同期処理の完了を待つこともできる。

      init() {
        Task {
          do {
            codes = try await Networker.queryCodeTbl(
              DBPath.dbPath(1), DBPath.dbPath(2))
          } catch {
            print("error: \(error)")
          }      
        }
        sleep(1)
      }