Swift Concurrency: Feedback Wanted!

SQLite requires all it's calls be made on the same thread, that's why I want my class to be a subclass of Thread and run the command serially through the main method. I already have this working pre-async/await. Just thinking about how I can convert it.

3 Likes

You may be interested in the proposal for custom executors. Requiring all work done on an actor's state to happen on a specific thread is very similar to the MainActor global actor.

5 Likes

While trying out the concurrency features by way of playing around with a simple iOS application, I think I have found a bug in the URLSession async additions (specifically the data(from:delegate:) function, but I have not done comprehensive testing). Is it meaningful to post about it here, or should I just file a bug report at Apple?

Is there any update on the interaction between async/await and result builders? I remember this point being raised in some proposal thread, but I don’t know if it was addressed and, if so, how.

Probably best to report using the usual channels: feedback for apple frameworks incl. foundation, and bugs.swift.org for swift issues. Thanks in advance.

I have a question about cancellation here. Grateful for any help. Thanks!

Concurrency works really great, amazing work thank you!!

My feedback so far is regarding error handling. Still feels a little archaic and hoping strongly-typed errors be revisited in light of Concurrency. This was discussed in various threads over the years, but a throw to a general purpose Error is really difficult for the callers to handle errors without knowing the implementation details and feels like a gap. This could finally rid the need of the well served Result type once and for all.

Going from this:

do {
    try await performWork()
} catch let error as MyError {
    switch error { // Exhaust all error cases
        case .invalidUser: ...
        case .expiredToken: ...
        case .etc: ...
    }
} catch {
    // Will never reach here but need to satisfy compiler
}

To this:

do {
    try await performWork()
} catch .invalidUser {
    ...
} catch .expiredToken {
    ...
} catch .etc {
    ...
} 

Also another completely different feedback I have is I noticed crashes while mixing GCD. I understand this is not the intended use case, but I think this will be unavoidable when async tasks performs work using dependencies and SDKs where we have no control or knowledge of the underlying implementation. I'm wondering if changing the GCD under the hood to switch between legacy and async/await implementation based on the caller would prevent these crashes?

4 Likes

catch .invalidUser

Just to clarify, one can catch MyError.invalidUser already. They will just need to specify the full type, and have a blanket catch statement if they wish to be exhaustive.

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

2 Likes

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.

1 Like

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()
}
2 Likes

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.

Doug

3 Likes

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.

Doug

2 Likes

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?

Here are some sudo samples:

func start() async {
    ...
    cancellable = myCombine.sink {...}
}

Doesn't fire in some scenarios, so seems I have to do this :woozy_face::

func start() async {
    ...
    DispatchQueue.main.async {
        self.cancellable = myCombine.sink {...}
    }
}

And this:

func start() async {
    DispatchQueue.customQueue.sync {...} // Crashes or race conditions sanitizer complaints
}

Or this:

cancellable = myCombine
    .sink {
       async { await start() } } // Continuation isn't reliable
    } 

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.

1 Like

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.

6 Likes

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.

2 Likes

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:

self.taskHandle?.cancel()
self.taskHandle = async {
    do {
        self.result = .success(try await self.search(searchText: value))
    } catch {
        self.result = .failure(error)
    }
}

I'd like to be able to do this:

self.taskHandle?.cancel()
self.taskHandle = async {
    self.result = Result(catching:  { try await self.search(searchText: value) })
}

But I get this error:

Cannot pass function of type '() async throws -> [Place]' to parameter expecting synchronous function type```

Result could be extended with an async initialiser — something like this:

extension Result where Failure == Error {
  init(awaiting task: () async throws -> Success) async {
    do {
      self = .success(try await task())
    } catch {
      self = .failure(error)
    }
  }
}

self.taskHandle = async {
  self.result = await Result {
    try await search(searchText: value)
  }
}
5 Likes

Thanks, I will do that. In terms of feedback, I guess it would be nice if it was built-in.

2 Likes

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)
Terms of Service

Privacy Policy

Cookie Policy