swiftswift-concurrencyswift6

Swift 6 concurrency pass an array to MainActor


class Something {
    var things: [String] = []
    
    func doit() {
        let newMessages: [String] = []
        Task { @MainActor in
            self.things = newMessages
        }
    }
}

This code fails to compile with swift 6 saying Sending 'newMessages' risks causing data races -- how can I move an array to the MainActor in a safe way?

UPDATE: it seems the capture is a red herring and actually I can't use self from MainActor at all?

class Something {
    func doit() {
        Task { @MainActor in
            self.doit()
        }
    }
}

give me error Task or actor isolated value cannot be sent


Solution

  • The issue is that Something/self isn't "safe". It could be attached to any actor or nonisolated. There are many ways to make it safe.

    The 2 main ways to make something safe is with Sendable or globalActor.

    The "easy" solution is to put Something on the MainActor so MainActor handles safety, this isn't ideal because we don't want to clutter the main.

    @MainActor
    class Something {
        var things: [String] = []
        
        func doit() async {
            let newMessages: [String] = await [] //await is to highlight that the work should be done async.
            self.things = newMessages
        }
    }
    

    Another option is to use actor which is usually appropriate for services.

    actor Something {
        var things: [String] = []
        
        func doit() async {
            let newMessages: [String] = []
            self.things = newMessages
        }
    }
    

    The 3rd option is to use a custom globalActor which is very convenient for things that use delegates or other non isolated methods

    @LocationActor
    class Something {
        var things: [String] = []
        
        func doit() async {
            let newMessages: [String] = []
            self.things = newMessages
        }
        
        nonisolated func doSomethingElse() {//This function is not isolated
            let newMessages: [String] = []
    
            Task { @LocationActor in // Safely modify the property
                self.things = newMessages
    
            }
        }
    }
    
    @globalActor
    struct LocationActor {
        static let shared: LocationActor = .init()
        
        typealias ActorType = LocationActor
        
        actor LocationActor {
            
        }
    }
    
    
    

    The 4th option is to use a struct that can be marked Sendable.

    struct Something: Sendable {
        var things: [String] = []
        
        mutating func doit() async {
            let newMessages: [String] = []
            self.things = newMessages
        }
    }
    

    Which is "right" depends on your use case.