4

Integrating actors with existing code doesn't really seem to be as simple as Apple wants you to believe. Consider the following simple actor:

actor Foo {
    var value: Int = 0
}

Trying to access this property from any AppKit/UIKit (task-less) controller just can't work because every Task is asynchronous.

class AppKitController {

    func myTaskLessFunc() {
        let f = Foo()
        var v: Int = -1
        Task { v = await f.value }
    }
}

This will give you the expected error Mutation of captured var 'v' in concurrently-executing code, also if you tried to lock this it wouldn't work. nonisolated actor methods also don't help, since you will run into the same problem.

So how do you read actor properties synchronous in a task-less context at all?

George
  • 25,988
  • 10
  • 79
  • 133
  • This is what an actor is for - to prevent you accessing a property etc. during execution of some actor method. Imagine if there was a transaction in place, where an actor method added `10` to `value`, then subtracted `10` again. If you accessed it synchronously, you _could_ be accessing it while `value` is `10`. So you should be reading it asynchronously. In your `myTaskLessFunc` example, the `v` won't be set be the time the function has finished executing. – George Dec 13 '21 at 10:42
  • I know how actors work, but unless there is a way to access the property from a non async context, whats the point? At some point you have to transfer a result outside of the actor. Then why use actors at all when I have to fallback to classes/locking anyway? –  Dec 13 '21 at 12:22
  • You could possibly try `nonisolated`: [reference](https://www.hackingwithswift.com/quick-start/concurrency/how-to-make-parts-of-an-actor-nonisolated) – George Dec 13 '21 at 12:25
  • Like I wrote, `nonisolated` is also entirely pointless because it just moves the problem to a different location. I would not be able to access the actor property in a `nonisolated` func as well. –  Dec 13 '21 at 12:29
  • Sorry, missed that part. Yeah that's true. What's limiting `myTaskLessFunc` from being `async` in this case? – George Dec 13 '21 at 12:30
  • If it were an `IBAction`, data source or other delegate for example. In that case you would have to litter `Tasks` all over the codebase by wrapping every IBAction method body in a `Task` (which is what I wanted to avoid). –  Dec 13 '21 at 12:32
  • 1
    From [@matt](https://stackoverflow.com/users/341994/matt), see chapter 'Taking a stand' from [Swift 5.5: Replacing GCD With Async/Await](https://www.biteinteractive.com/swift-5-5-replacing-gcd-with-async-await/). Looks like the best way is just to have a few `Task`s. After all, I don't really see logically how there is a way to get an asynchronous value from a synchronous context. Similar problem to the ol' `return`ing from a function within a callback closure. Having a few `Task { ... }` can't hurt for those functions that _must_ be synchronous. – George Dec 13 '21 at 12:43
  • It’s annoying having to propagate async code everywhere, but that’s the reality we ignore when writing concurrent code –another is transferring non Sendable between threads all the time. It’s fine to mix sync code with async code, using combine is async with another name. – Jano Dec 16 '21 at 11:21
  • What's really needed is a sync {} context similar to async {}. Yes, I realize that my current thread may block on the await... but that's what I want to happen. – Michael Long Sep 14 '22 at 21:31

1 Answers1

8

I found a way to do it by using Combine, like this:

import Combine

actor Foo {

    nonisolated let valuePublisher = CurrentValueSubject<Int, Never>(0)

    var value: Int = 0 {
        didSet {
            valuePublisher.value = value
        }
    }
}

By providing an non-isolated publisher, we can propagate the actor value safely to the outside, since Publishers are thread safe.

Callers can either access foo.valuePublisher.value or subscribe to it from the outside, without requiring an async context.