Thank you for this incredible achievement. I do feel actor reentrancy may become problematic when “obvious” extensions to basic actor use cases does not work. A good example is what was demonstrated in the WWDC video: an attempted improvement to caching causes a subtle bug. The fix itself — to add another type of state (the handle) — seems to put the high-level comfort of the actors on shaky ground; your reasoning must now interleave alongside the execution. “Breaking the illusion” to borrow Dave Abrahams’s phrasing
Could there be a way for the compiler to assist in this? E.g. once you add an await to your function, the properties in the actor now require you to consider them in terms of handles. Basically compiler assistance to guide you to the solution described in the WWDC video
I also feel uneasy about support for reentrancy but as I understand it, it's a trade off. Without reentrancy, the concurrency runtime would have to spawn arbitrary numbers of threads in order to guarantee forward progress, which could lead to thread explosions. With the fixed set of threads that the default runtime manages you need reentrancy to provide this guarantee. It should ultimately be the faster approach (I believe) but with potential for subtle bugs. Hopefully, as you say, the compiler will soon be able to help out.
Actors conforming to protocols is still pretty awkward, especially for protocols with defaulted implementations, like Hashable. Implementing hash(into:) is fairly straightforward but doesn't let the default implementation of hashValue work correctly, leading to a compiler error on a property I don't even implement. Implementing both works, but it's hard to say whether it's correct.
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
nonisolated
var hashValue: Int {
var hasher = Hasher()
hasher.combine(id)
return hasher.finalize()
}
As we gain more experience with the actor model, we could probably find common problematic patterns that the compiler or some kind of static analysis tool could diagnose.
That the auto-synthesized hashValue doesn't work is definitely a bug. Synthesizing hash(into:) is actually quite interesting, because you can only sensibly use the let properties to do it.
Just want to start off by saying I'm loving all the code and indentations I'm deleting from my branch as I refactor with Concurrency. Really looking forward to AsyncStream integration with core libraries like CoreLocation, Bluetooth, etc which seems like a match made in heaven.
Now the unfun part.. I'm finding a lot of strange and inconsistent behaviour mixing Combine, GCD, and Concurrency, such as Combine events not triggering, GCD crashing, and Concurrency not continuing. I know a mixed bag isn't ideal, but reasonably unavoidable. Is it possible to create an official document of what can and can't be mixed between these 3 technologies, and if one is required to do so, what are the strategies and nuances we should be aware of?
I would love to never use GCD and Combine ever again in favour of Concurrency, but doesn't seem realistic for a another few years to flush it out. Can maybe adding async helpers directly to Combine and GCD help or at least a guideline for a safe approach to mixing them.
Ideally there should not be any, and we're very interested in hearing more about problems you're running into mixing these technologies. If you're able to file bugs with complete examples of code that isn't doing what you expect, we'd like to take a look and debug what's going on.
Those samples all should work; do you have feedbacks or bug reports filed w/ more detailed examples? Combine itself does not really do anything that outlandish w.r.t. threading/queues so it might be a more general bug.
With integration into UI, I need cancellation and also like to use result as setting the value for the success and failure is atomic. And often we need to display the result or deal with the error (not ignore it).
So I'm finding myself writing code like this often:
To be honest, I don't know if this is already reported:
Using the ?? operator does not work without parentheses:
let newTodos = try? await viewModel.refreshTodos() ?? [] // Left side of nil coalescing operator '??' has non-optional type '[Todo]', so the right side is never used
saveNewTodos(todos: newTodos) // Value of optional type '[Todo]?' must be unwrapped to a value of type '[Todo]'
Workaround:
let newTodos = (try? await viewModel.refreshTodos()) ?? [] // no error
saveNewTodos(todos: newTodos)
Expected:
let newTodos = try? await viewModel.refreshTodos() ?? []
saveNewTodos(todos: newTodos)
This is correct as-is and has nothing to do with async/await. Without parentheses, try? applies to the rest of the line, producing an optional result, and the left-hand side of the ?? has non-optional type, exactly as the diagnostic tells you.
If you want try? to apply only to the left-hand side of the ?? operator, use parentheses just as you show—this is no different from using parentheses to perform addition before multiplication. It’s not a workaround for some bug, it’s just how operator precedence works.
Maybe this isn't the right place to put this but it seems like the concurrency implementation was only partially implemented. Perhaps this is documented elsewhere that some of the functionality is to be implemented later on in the process, but as an example, swift-corelibs-foundation does not provide async/await APIs, which means it cannot be used on URLSession methods outside of Apple platforms.
Again, maybe this is something to be expected, but given the push for Swift adoption outside of Apple platforms, I would have thought that this functionality would be able to be experimented with on nightly 5.5 builds/docker images.
I've been thinking about this and this solution seems like overkill for my situation. All I need is an @Actor that can guarantee that it's always on the same thread. Maybe @ThreadActor.
This is a limitation of the current compiler and XCTest's APIs. XCTest's assertions only take auto closures, which aren't marked async, and so you can't use async APIs in them directly. If the 306 amendment is accepted, XCTest could update with async overloads of the assertions, which we could see in a month or two.
I know Combine is an Apple framework, but it would be nice if there was some integration with Combine and async/await. For example, this first async throws getter would be really helpful as a built in async property of Publisher, as I'm sure there are issues with my attempt:
enum AsyncError: Error {
case valueWasNotEmittedBeforeCompletion
}
class CancellableWrapper {
var cancellable: AnyCancellable?
}
extension Publisher {
var first: Output {
get async throws {
// Variable tracks if we sent a value or not.
var didSendValue: Bool = false
let cancellableWrapper = CancellableWrapper()
return try await withTaskCancellationHandler {
cancellableWrapper.cancellable?.cancel()
} operation: {
// This check is necessary in case this code runs after the task was
// cancelled. In which case we want to bail right away.
try Task.checkCancellation()
return try await withUnsafeThrowingContinuation { continuation in
// This check is necessary in case this code runs after the task was
// cancelled. In which case we want to bail right away.
guard !Task.isCancelled else {
continuation.resume(throwing: Task.CancellationError())
return
}
cancellableWrapper.cancellable =
handleEvents(receiveCancel: {
// We don't get a cancel error when cancelling a publisher, so we need
// to handle if the publisher was cancelled from the
// `withTaskCancellationHandler` here.
continuation.resume(throwing: Task.CancellationError())
}).sink { completion in
if case let .failure(error) = completion {
continuation.resume(throwing: error)
} else if !didSendValue {
continuation.resume(throwing: AsyncError.valueWasNotEmittedBeforeCompletion)
}
} receiveValue: { value in
continuation.resume(with: .success(value))
didSendValue = true
}
}
}
}
}
}
Also, the ability to turn a Publisher into an AsyncSequence or AsyncThrowingSequence would be very helpful as well.
Classes with mutable state are not safe to send across concurrency domains, correct?
To send something across it should conform to Sendable, correct?
If the class Foo conforms to Sendable the compiler will emit errors about Foo.value being mutable.
If the class however doesn't conform, no errors are emitted and the code below runs just fine.
But it's actually not safe from data races, is it?
I expected an error as well if Foo doesn't conform to Sendable and if it's used across concurrency domains. Or was I wrong to expect this?
Using xcode beta 2.
@main
struct MyApp
{
static func main() async
{
let foo = Foo(value: "foo")
let fooActor = FooActor()
let barActor = BarActor()
await barActor.use(foo: foo)
await fooActor.use(foo: foo)
await barActor.send(foo: foo, to: fooActor)
}
}
final class Foo //: Sendable
{
var value: String
init(value: String)
{
self.value = value
}
}
actor FooActor
{
func use(foo: Foo)
{
print("foo actor using: \(foo.value)")
}
}
actor BarActor
{
func send(foo: Foo, to target: FooActor) async
{
foo.value = "visited bar"
await target.use(foo: foo)
}
func use(foo: Foo)
{
print("bar actor using: \(foo.value)")
}
}