0

Consider the following function snippet:

public func update(tables: [LocalTableItem]) async throws -> UpdateResult {
        
        let result = try await store.remove(tables: tables)
        print("** removing table \(tables.last!.name)")
        
        switch result {
        case .success:
            let insertResult = try await self.store.insert(tables: tables)
            print("** inserting table \(tables.last!.name)")

when called in a concurrent manner:

 Task {
            _ = try await store.update(tables: [updatedTable])
        }

I observe in the Log:

** removing table icradle_loler_engineer.
** removing table icradle_loler_engineer.
** inserting table icradle_loler_engineer.
** inserting table icradle_loler_engineer.

The remove and insert calls are no longer balanced and duplicate entries will be seen in the DB.

Is there a way to make this async function thread safe?

I have tried making the class an Actor although my understanding may be incomplete, as this did not resolve the issue.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116

1 Answers1

0

This is a good use case for an AsyncChannel, part of Apple’s Swift Async Algorithms package.

E.g., have a task to process elements posted to a channel:

let channel = AsyncChannel<[LocalTableItem]>()

func monitorChannel() async { 
    for await items in channel {
        try? await store.update(tables: items)
    }
}

And then to add an array of items to the channel:

Task {
    await channel.send([updatedTable])
}

In SwiftUI you might start monitorChannel in .task view modifier (which has the virtue of canceling the tasks when the view is dismissed). In AppKit/UIKit, you would manually start the task when the view appears and cancel it when it disappears:

var task: Task<Void, Never>?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    task = Task { await monitorChannel() }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    task?.cancel()
}

Or, if you want it to stop if an error is thrown:

let channel = AsyncThrowingChannel<[LocalTableItem], Error>()

func monitorChannel() async throws {
    for try await items in channel {
        try await store.update(tables: items)
    }
}

And

var task: Task<Void, Error>?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    task = Task { try await monitorChannel() }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    task?.cancel()
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044