Swift Concurrency: Feedback Wanted!

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)

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.

4 Likes

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.

I had a thread here referencing this How to use async/await w/ docker.

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.

2 Likes

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.

If we could have a @ThreadActor attribute to guarantee that this actor type is always running on the same thread, then it would work.

In the Meet async/await in Swift - WWDC 2021 - Videos - Apple Developer session they mention that we could now use async-await in unit tests, an easy way is to use the XCTAssertNoThrow function to assert that the task didn't throw an error.

Snippet from the session:

    func testFetchThumbnails() async throws {
        XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
    }

I tried something similar but I am receiving this compile error
'async' call in an autoclosure that does not support concurrency

However, when I wrap the code inside XCTAssertNoThrow with an unstructured task (async), it will work just fine.

it seems that somehow the compiler can't detect that I am in an async function already in unit tests inside XCTAssertNoThrow?

1 Like

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.

2 Likes

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.

3 Likes

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)")
    }
}

In Swift 5.5 we’re not fully enforcing the Sendable checks, we will enable these in Swift 6 — see here for a detailed writeup:

2 Likes

You need @unchecked Sendable to make something Sendable that the compiler doesn't see as obviously true.

From the post @ktoso linked to, you want to use the -warn-concurrency compiler option to get the behavior you're looking for.

1 Like

A few comments from an afternoon of converting code over:

  • TaskGroup (or at least TaskGroup<Void>) could use a wait() function that waits for all elements in the group rather than immediately cancelling.

  • The lack of reasync functions for things like withUnsafePointer, withExtendedLifetime, Optional.map, and Sequence operations currently is a real pain point. I would say that pretty much every rethrows function in the standard library or Foundation should also be reasync.

  • As far as I'm aware, we don't have a good solution for making async versions of protocols.
    For example, ideally there'd be an AsyncDecodable protocol, which carries the same requirements as Decodable except with an async signature (i.e. init(from decoder: Decoder) async throws). I wonder if that's a common enough need (a variant of an existing protocol whose methods and closure parameters are async when called from an async context) that it deserves special treatment; e.g. async Codable could be a distinct subtype of Codable. I'm not sure how that would scale in terms of code size – from my limited understanding, doubling the number of protocol conformances for each app seems a heavy price to pay.

  • As a possible alternative solution for the async protocol issue, it would be very useful to have an escape hatch for a sync function to turn into an async function. I realise that this means there could either be suspension points in non-awaited code or blocking/potentially deadlocked code; however, given we don't have the ability to make all the protocols or closure-taking functions in all the external libraries that we use async there's sometimes no other real option. Hacking around this with a DispatchGroup.wait() doesn't seem ideal, so if a more efficient option is implementable in the runtime then it would be great to add.

  • Now that async get vars are allowed, it would be nice to support static lets which are assigned with the result of an async expression.

  • If it's not already, Void should be @Sendable. An earlier build rejected Void as e.g. a TaskGroup result type, but I'm not sure if the reason it's allowed now is because checking has been relaxed for Swift 5.5 or because the @Sendable conformance has been added.

  • Probably just a bug, but @unchecked Sendable doesn't currently work; you have to use UnsafeSendable instead.

2 Likes

If you exit the task group without throwing, it should await pending child tasks without cancellation.

2 Likes

Okay, cool, that makes sense – I probably just got mixed up with the proposed rules for async let and didn’t properly test.

1 Like

I’m confused. Can’t you just call synchronous functions inside async {} to do that?

I think that only works if you don't need whatever inside the async block to complete before the sync function returns.

Terms of Service

Privacy Policy

Cookie Policy