In Swift, let’s say we have an actor, which has a private struct.
We want this actor to be reactive, and to give access to a publisher that publishes a specific field of the private struct.
The following code seems to work. But it produces a warning I do not understand.
public actor MyActor {
private struct MyStruct {
var publicField: String
}
#Published
private var myStruct: MyStruct?
/* We force a non isolated so the property is still accessible from other contexts w/o await in other modules. */
public nonisolated let publicFieldPublisher: AnyPublisher<String?, Never>
init() {
self.publicFieldPublisher = _myStruct.projectedValue.map{ $0?.publicField }.eraseToAnyPublisher()
}
}
The warning:
Actor 'self' can only be passed 'inout' from an async initializer
What does that mean? Is it possible to get rid of this warning? Is it “dangerous” (can this cause issues later)?
Note:
If I use $myStruct instead of _myStruct.projectedValue, it does not compile at all. I think it’s related, but I don’t truly see how.
The error is in that case:
'self' used in property access '$myStruct' before all stored properties are initialized
The warning is because when you are using _myStruct.projectedValue compiler notes that you are trying to do mutation on the actor from a synchronous context. All you have to do to remove the warning is to make your initializer asynchronous using async specifier.
The actor initializer proposal goes into more detail for why initializer needs to be asynchronous if there mutations inside actor initializer. Cosider the following case:
actor Clicker {
var count: Int
func click() { self.count += 1 }
init(bad: Void) {
self.count = 0
// no actor hop happens, because non-async init.
Task { await self.click() }
self.click() // 💥 this mutation races with the task!
print(self.count) // 💥 Can print 1 or 2!
}
}
Since there are two click method call one from Task and one called directly, there might be a case that the Task call executed first and hence the next call need to wait until that finishes.
Related
#globalActor
actor LibraryAccount {
static let shared = LibraryAccount()
var booksOnLoan: [Book] = [Book()]
func getBook() -> Book {
return booksOnLoan[0]
}
}
class Book {
var title: String = "ABC"
}
func test() async {
let handle = Task { #LibraryAccount in
let b = await LibraryAccount.shared.getBook() // WARNING and even ERROR without await (although function is not async)
print(b.title)
}
}
This code generates the following warning:
Non-sendable type 'Book' returned by call to actor-isolated instance method 'getBook()' cannot cross actor boundary
However, the closure is itself marked with the same global Actor, there should be no Actor boundary here. Interestingly, when removing the await from the offending line, it will emit an error:
Expression is 'async' but is not marked with 'await'
This looks like it does not recognize that this is the same instance of the Actor as the one guarding the closure.
What's going on here? Am I misunderstanding how GlobalActors work or is this a bug?
Global actors aren't really designed to be used this way.
The type on which you mark #globalActor, is just a marker type, providing a shared property which returns the actual actor instance doing the synchronisation.
As the proposal puts it:
A global actor type can be a struct, enum, actor, or final class. It is essentially just a marker type that provides access to the actual shared actor instance via shared. The shared instance is a globally-unique actor instance that becomes synonymous with the global actor type, and will be used for synchronizing access to any code or data that is annotated with the global actor.
Therefore, what you write after static let shared = doesn't necessarily have to be LibraryAccount(), it can be any actor in the world. The type marked as #globalActor doesn't even need to be an actor itself.
So from Swift's perspective, it's not at all obvious that the LibraryAccount global actor is the same actor as any actor-typed expression you write in code, like LibraryAccount.shared. You might have implemented shared so that it returns a different instance of LibraryAccount the second time you call it, who knows? Static analysis only goes so far.
What Swift does know, is that two things marked #LibraryAccount are isolated to the same actor - i.e. this is purely nominal. After all, the original motivation for global actors was to easily mark things that need to be run on the main thread with #MainActor. Quote (emphasis mine):
The primary motivation for global actors is the main actor, and the
semantics of this feature are tuned to the needs of main-thread
execution. We know abstractly that there are other similar use cases,
but it's possible that global actors aren't the right match for those
use cases.
You are supposed to create a "marker" type, with almost nothing in it - that is the global actor. And isolate your business logic to the global actor by marking it.
In your case, you can rename LibraryAccount to LibraryAccountActor, then move your properties and methods to a LibraryAccount class, marked with #LibraryAccountActor:
#globalActor
actor LibraryAccountActor {
static let shared = LibraryAccountActor()
}
#LibraryAccountActor
class LibraryAccount {
static let shared = LibraryAccount()
var booksOnLoan: [Book] = [Book()]
func getBook() async -> Book { // this doesn't need to be async, does it?
return booksOnLoan[0]
}
}
class Book {
var title: String = "ABC"
}
func test() async {
let handle = Task { #LibraryAccountActor in
let b = await LibraryAccount.shared.getBook()
print(b.title)
}
}
With the PromiseKit library, it’s possible to create a promise and a resolver function together and store them on an instance of a class:
class ExampleClass {
// Promise and resolver for the top news headline, as obtained from
// some web service.
private let (headlinePromise, headlineSeal) = Promise<String>.pending()
}
Like any promise, we can chain off of headlinePromise to do some work once the value is available:
headlinePromise.get { headline in
updateUI(headline: headline)
}
// Some other stuff here
Since the promise has not been resolved yet, the contents of the get closure will be enqueued somewhere and control will immediately move to the “some other stuff here” section; updateUI will not be called unless and until the promise is resolved.
To resolve the promise, an instance method can call headlineSeal:
makeNetworkRequest("https://news.example/headline").get { headline in
headlineSeal.fulfill(headline)
}
The promise is now resolved, and any promise chains that had been waiting for headlinePromise will continue. For the rest of the life of this ExampleClass instance, any promise chain starting like
headlinePromise.get { headline in
// ...
}
will immediately begin executing. (“Immediately” might mean “right now, synchronously,” or it might mean “on the next run of the event loop”; the distinction isn’t important for me here.) Since promises can only be resolved once, any future calls to headlineSeal.fulfill(_:) or headlineSeal.reject(_:) will be no-ops.
Question
How can this pattern be translated idiomatically into Swift concurrency (“async/await”)? It’s not important that there be an object called a “promise” and a function called a “resolver”; what I’m looking for is a setup that has the following properties:
It’s possible for some code to declare a dependency on some bit of asynchronously-available state, and yield until that state is available.
It’s possible for that state to be “fulfilled” from potentially any instance method.
Once the state is available, any future chains of code that depend on that state are able to run right away.
Once the state is available, its value is immutable; the state cannot become unavailable again, nor can its value be changed.
I think that some of these can be accomplished by storing an instance variable
private let headlineTask: Task<String, Error>
and then waiting for the value with
let headline = try await headlineTask.value
but I’m not sure how that Task should be initialized or how it should be “fulfilled.”
Here is a way to reproduce a Promise which can be awaited by multiple consumers and fulfilled by any synchronous code:
public final class Promise<Success: Sendable>: Sendable {
typealias Waiter = CheckedContinuation<Success, Never>
struct State {
var waiters = [Waiter]()
var result: Success? = nil
}
private let state = ManagedCriticalState(State())
public init(_ elementType: Success.Type = Success.self) { }
#discardableResult
public func fulfill(with value: Success) -> Bool {
return state.withCriticalRegion { state in
if state.result == nil {
state.result = value
for waiters in state.waiters {
waiters.resume(returning: value)
}
state.waiters.removeAll()
return false
}
return true
}
}
public var value: Success {
get async {
await withCheckedContinuation { continuation in
state.withCriticalRegion { state in
if let result = state.result {
continuation.resume(returning: result)
} else {
state.waiters.append(continuation)
}
}
}
}
}
}
extension Promise where Success == Void {
func fulfill() -> Bool {
return fulfill(with: ())
}
}
The ManagedCriticalState type can be found in this file from the SwiftAsyncAlgorithms package.
I think I got the implementation safe and correct but if someone finds an error I'll update the answer. For reference I got inspired by AsyncChannel and this blog post.
You can use it like this:
#main
enum App {
static func main() async throws {
let promise = Promise(String.self)
// Delayed fulfilling.
let fulfiller = Task.detached {
print("Starting to wait...")
try await Task.sleep(nanoseconds: 2_000_000_000)
print("Promise fulfilled")
promise.fulfill(with: "Done!")
}
let consumer = Task.detached {
await (print("Promise resolved to '\(promise.value)'"))
}
// Launch concurrent consumer and producer
// and wait for them to complete.
try await fulfiller.value
await consumer.value
// A promise can be fulfilled only once and
// subsequent calls to `.value` immediatly return
// with the previously resolved value.
promise.fulfill(with: "Ooops")
await (print("Promise still resolved to '\(promise.value)'"))
}
}
Short explanation
In Swift Concurrency, the high-level Task type resembles a Future/Promise (it can be awaited and suspends execution until resolved) but the actual resolution cannot be controlled from the outside: one must compose built-in lower-level asynchronous functions such as URLSession.data() or Task.sleep().
However, Swift Concurrency provides a (Checked|Unsafe)Continuation type which basically act as a Promise resolver. It is a low-lever building block which purpose is to migrate regular asynchronous code (callback-based for instance) to the Swift Concurrency world.
In the above code, continuations are created by the consumers (via the .value property) and stored in the Promise. Later, when the result is available the stored continuations are fulfilled (with .resume()), which resumes the execution of the consumers. The result is also cached so that if it is already available when .value is called it is directly returned to the called.
When a Promise is fulfilled multiple times, the current behavior is to ignore subsequent calls and to return aa boolean value indicating if the Promise was already fulfilled. Other API's could be used (a trap, throwing an error, etc.).
The internal mutable state of the Promise must be protected from concurrent accesses since multiple concurrency domains could try to read and write from it at the same time. This is achieve with regular locking (I believe this could have been achieved with an actor, though).
I recently came across the nonisolated keyword in Swift in the following code:
actor BankAccount {
init(balance: Double) {
self.balance = balance
}
private var balance: Double
nonisolated func availableCountries() -> [String] {
return ["US", "CA", "NL"]
}
func increaseBalance(by amount: Double) {
self.balance += amount
}
}
What does it do? The code compiles both with and without it.
The nonisolated keyword was introduced in SE-313. It is used to indicate to the compiler that the code inside the method is not accessing (either reading or writing) any of the mutable state inside the actor. This in turn, enables us to call the method from non-async contexts.
The availableCountries() method can be made nonisolated because it doesn't depend on any mutable state inside the actor (for example, it doesn't depend on the balance variable).
When you are accessing different methods or variables of an actor, from outside the actor, you are only able to do so from inside an asynchronous context.
Let's remove the nonisolated keyword from the availableCountries() method. Now, if we want to use the method, we are only allowed to do so in an async context. For example, inside a Task:
let bankAccount = BankAccount(balance: 1000)
override func viewDidLoad() {
super.viewDidLoad()
Task {
let availableCountries = await bankAccount.availableCountries()
print(availableCountries)
}
}
If we try to use the method outside of an async context, the compiler gives us errors:
However, the availableCountries() isn't using any mutable state from the actor. Thus, we can make it nonisolated, and only then we can call it from non async contexts (and the compiler won't nag).
I'm new (like most everyone, I suppose) to Swift concurrency, and I'm running into a compiler error I don't know what to do with.
struct Thing {
var counter = 0
mutating func increment() async {
counter += 1
}
}
class Controller: UIViewController {
var thing = Thing()
func mutate() async {
await thing.increment()
print(thing.counter)
}
}
let c = Controller()
Task {
await c.mutate()
}
The first line of the mutate() function gives me the following error.
Actor-isolated property 'thing' cannot be passed 'inout' to 'async' function call
If I just inherit from class instead of UIViewController things work fine, but I need the controller here, so I need to figure out how to make this work in that specific context.
I think the issue comes from Thing being a struct. A mutating func on a struct will assign a new value to the thing property on the Controller. In order for that to work, thing is treated as an inout parameter in the call to thing.increment().
If you make thing an actor instead of a struct then increment()won't need to be a mutating func and so thing won't be treated as an inout parameter.
A possible workaround is to make a copy of the struct first, then call the mutating func on the copy, then store it back on the property in the controller.
func mutate() async {
var thing = self.thing
await thing.increment()
self.thing = thing
print(thing.counter)
}
The reason it's an issue is the UIViewControllers are all actors now, so the properties are considered actor isolated. There is a nonisolated keyword but it cant be applied to stored properties so it doesn't seem to help here.
If the controller is changed to be an actor, the error message changes a bit to say that.
error: cannot call mutating async function 'increment()' on actor-isolated property 'thing'
await thing.increment()
^
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?
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.