Home > OS >  MainActor and async await when reading and writing
MainActor and async await when reading and writing

Time:03-05

I understand the new async syntax in Swift in the sense that if I call it, then it will handle a pool of asynchronous queues / threads (whatever) to do the work. What I don't understand is how we return to the main thread once it's all over.

// On main thread now

let manager = StorageManager()
let items = await manager.fetch // returns on main thread?

struct StorageManager {
    
    private func read() throws -> [Item] {
        let data = try file.read()
        if data.isEmpty { return [] }
        return try JSONDecoder().decode([Item].self, from: data)
    }
    
    func fetch() async {
        fetchAndWait()
    }
    
    func fetchAndWait() {
        if isPreview { return }
        let items = try? read()
        fetchedItems = items ?? []
    }
    
    func save() throws {
        let data = try JSONEncoder().encode(fetchedItems)
        try file.write(data)
    }
}

I want to make sure that I read and write from/to disk in the correct way i.e. is thread safe when necessary and concurrent where possible. Is it best to declare this struct as a @MainActor ?

CodePudding user response:

There is nothing in the code you've given that uses async or await meaningfully, and there is nothing in the code you've given that goes onto a "background thread", so the question as posed is more or less meaningless. If the question did have meaning, the answer would be: to guarantee that code doesn't run on the main thread, put that code into an actor. To guarantee that code does run on the main thread, put that code into a @MainActor object (or call MainActor.run).

CodePudding user response:

The async methods do not return automatically to the main thread, they either:

  1. complete in the background whatever they are doing

or

  1. explicitly pass at a certain moment the execution to the main thread using, for example, DispatchQueue.

In the code above you can start by correcting the fact that fetch() does not return any value (items will receive nothing based on your code).

Example of your code for case 1 above:

let manager = StorageManager()
let items = await manager.fetch // not on the main thread, the value will be stored in the background

struct StorageManager {
    
    private func read() throws -> [Item] {
        let data = try file.read()
        if data.isEmpty { return [] }
        return try JSONDecoder().decode([Item].self, from: data)
    }
    
    func fetch() async -> [Item] {
        if isPreview { return }
        let items = try? read()
        return items ?? []
    }
    
    func save() throws {
        let data = try JSONEncoder().encode(fetchedItems)
        try file.write(data)
    }
}

Example for case 2 above (I created an @Published var, which should only be written on the main thread, to give you the example):

class ViewModel: ObservableObject {

    let manager = StorageManager()
    @Published var items = [Item]() // should change value only on main thread

    func updateItems() {
        Task {            // Enter background thread
            let fetchedItems = await self.manager.fetch()
            DispatchQueue.main.async {         // Back to main thread
                self.items = fetchedItems
            }
        }
    }

}

struct StorageManager {
    
    private func read() throws -> [Item] {
        let data = try file.read()
        if data.isEmpty { return [] }
        return try JSONDecoder().decode([Item].self, from: data)
    }
    
    func fetch() async -> [Item] {
        if isPreview { return }
        let items = try? read()
        return items ?? []
    }
    
    func save() throws {
        let data = try JSONEncoder().encode(fetchedItems)
        try file.write(data)
    }
}
  • Related