swiftconcurrencyswift6

Actor-isolated property 'logs' can not be referenced from a nonisolated context


I am trying to learn Swift 6. The problem I've been facing is, I can't read (not trying to modify) the properties from actor on UI class.

Is it possible to read without adding extra stored properties or await?

Can you give me any solution for this?

final actor Logger: Sendable {
    
    static let current = Logger()
    
    private(set) final var logs: [Int] = []
    
    private init() { }
    
    nonisolated var count: Int {
        
        return self.logs.count
    }
}

final class ViewController: NSViewController, NSTableViewDataSource {
   
    @IBOutlet weak var tableView: NSTableView!
     
    func numberOfRows(in tableView: NSTableView) -> Int {
        
        return Logger.current.count
    }
}

Error:

Actor-isolated property 'logs' can not be referenced from a nonisolated context

Thanking you!


Solution

  • You can use a different synchronization mechanism if you want to access this from different concurrency contexts without await. For example, SE-0433 says:

    In concurrent programs, protecting shared mutable state is one of the core fundamental problems to ensuring reading and writing data is done in an explainable fashion. Synchronizing access to shared mutable state is not a new problem in Swift. We've introduced many features to help protect mutable data. Actors are a good default go-to solution for protecting mutable state because it isolates the stored data in its own domain. At any given point in time, only one task will be executing "on" the actor, and have exclusive access to it. Multiple tasks cannot access state protected by the actor at the same time, although they may interleave execution at potential suspension points (indicated by await). In general, the actor approach also lends itself well to code organization, since the actor's state, and operations on this state are logically declared in the same place: inside the actor.

    Not all code may be able (or want) to adopt actors. Reasons for this can be very varied, for example code may have to execute synchronously without any potential for other tasks interleaving with it. Or the async effect introduced on methods may prevent legacy code which cannot use Swift Concurrency from interacting with the protected state.

    Whatever the reason may be, it may not be feasible to use an actor. In such cases, Swift currently is missing standard tools for developers to ensure proper synchronization in their concurrent data-structures. Many Swift programs opt to use ad-hoc implementations of a mutual exclusion lock, or a mutex. A mutex is a simple to use synchronization primitive to help protect shared mutable data by ensuring that a single execution context has exclusive access to the related data. The main issue is that there isn't a single standardized implementation for this synchronization primitive resulting in everyone needing to roll their own.

    So, in Swift 6 one can use a Mutex:

    import Synchronization
    
    final class Logger: Sendable {
        static let current = Logger()
    
        private let logs = Mutex<[Int]>([])
    
        private init() { }
    
        var count: Int {
            logs.withLock { $0.count }
        }
    }
    

    On Apple platforms, that uses an unfair lock. And if you want to do this in earlier OS versions, you can replace the Mutex with an OSAllocatedUnfairLock:

    import os.lock
    
    final class Logger: Sendable {
        static let current = Logger()
    
        private let logs = OSAllocatedUnfairLock(initialState: [Int]())
    
        private init() { }
    
        var count: Int {
            logs.withLock { $0.count }
        }
    }
    

    Now, likely, you might want to access the “logs”, themselves: If so, you might just expose that, and retire individual computed properties like count, and the like, which introduce races, and, instead, just return the logs:

    import Synchronization
    
    final class Logger: Sendable {
        static let current = Logger()
    
        private let _logs = Mutex<[Int]>([])
    
        private init() { }
    
        var logs: [Int] {
            _logs.withLock { $0 }
        }
    
        func append(_ value: Int) {
            _logs.withLock { $0.append(value) }
        }
    }
    

    That way the caller gets a safe copy of the array, avoiding races between the fetch of the count and mutations of the collection that might be occurring on different threads.