New concurrency api not available in latest toolchain

When using the toolchain from April 10 the new concurrency api is annotated with

@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)

So it is not possible to try it out - is there a reason for that (and a workaround)?

2 Likes

I double this. Today's toolchain broke everything. It wasn't production code, of course, but I was just playing around with async stuff. Wrapping everything with available isn't really an option, tbh.

You can try it out if you annotate your own code with

@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)

And use

if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *) {
    detach {
        runYourAsyncCode()
    }
}

I guess this is here because we'll only be able to run new concurrency code on macOS, iOS versions which include the concurrency libraries. So only available iOS 15 or later, macOS future version and those version numbers have not been set yet.

1 Like

We can now use async/await without the flag -Xfrontend -enable-experimental-concurrency, however we can't call the function.
So if SE-0304 Structured concurrency is not implemented on Swift 5.5, we can't use async/await, though they are implemented.

Like I said, this is unacceptable — code size and complexity would become absurd. They could at least enhance -enable-experimental-concurrency flag so that it would bypass those @available checks.

I think you can pass -Xfrontend -disable-availability-checking to the compiler to disable availability checking, which should allow you to use the toolchain normally.

1 Like

Looks like using both -enable-experimental-concurrency and -disable-availability-checking get the async APIs back.

Anyone know what the replacement for @asyncHandler would be?

1 Like

Probably using detach { ... } in the body. My understanding is that @asyncHandler is just sugar over that. You can still use @asyncHandler for the time being if you pass -enable-experimental-async-handler but it is meant to be removed, though I don't know when.

Thanks for the tip. However, couldn't make it work in SPM:

swiftSettings: [
    .unsafeFlags([
        "-Xfrontend", "-enable-experimental-concurrency",
        "-Xfrontend", "-disable-availability-checking",
    ])
]

Didn't work at all for some reason, still having availability errors

It works in Xcode directly. :man_shrugging:

This doesn't seem to work for things like Obj-C delegate methods, so there's probably more that is needed.

I wonder why they want to remove @asyncHandler – I like how it discourages us from using detach(_:) throughout our code, meaning that people are more likely to stick to structured concurrency instead of starting new top-level tasks everywhere.

Also, @Douglas_Gregor said a while back that @asyncHandler is "more optimizable syntactic sugar" for detach(_:). So really it seemed like a win-win for me.

There are a lot of basic questions about @asyncHandler that we're not confident about right now:

  • The spelling doesn't seem great.
  • Async handlers make it really easy to accidentally break structured concurrency, since the handler task is detached. Perhaps the tasks shouldn't be fully detached and need some other form of structure.
  • Async handlers currently detach immediately, which is nice for certain use cases; however, the goal was originally to support the needs of synchronous UI handlers, which almost certainly want to do an initial segment of their work synchronously.
  • Relatedly, actor-isolated async handlers become non-actor-isolated externally, which doesn't really mesh with even allowing the preferred semantics for synchronous UI handlers.
4 Likes

Sure. In the meantime, what's the preferred solution for implementing the URLSessionDelegate with an actor? Or is that broken for the time being?

1 Like

I think we understand that URLSession will need some rethinking to fit more cleanly with async/await. I don't think it's impossible to make it work with structured concurrency instead of detached tasks, though, if you do some fairly subtle things with continuations. If you have a short sample, I can show you what I mean.

Sure, I had the following example working previously. However, without asyncHandler, there doesn't seem to be a way to get it to compile, as I get the error Actor-isolated instance method 'urlSession(_:task:didCompleteWithError:)' cannot be @objc, even when using detach internally.

actor Session {
    private(set) lazy var session = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil)
    private(set) lazy var sessionDelegate = SessionDelegate(self)
    
    private(set) var requests: [URLSessionTask: DataRequest] = [:]
    
    func request() async throws -> DataRequest {
        let request = DataRequest()
        let task = await request.task(using: session)
        requests[task] = request
        task.resume()
        
        return request
    }
    
    func request(for task: URLSessionTask) -> DataRequest? {
        requests[task]
    }
}

actor SessionDelegate: NSObject {
    private weak var owner: Session?
    
    init(_ owner: Session) {
        self.owner = owner
    }
}

actor DataRequest {
    private(set) var data = Data()
    private(set) var error: Error?
    private var completion: ((Data, Error?) -> Void)?
    
    func task(using session: URLSession) -> URLSessionTask {
        session.dataTask(with: URLRequest(url: URL(string: "https://httpbin.org/get")!))
    }
    
    func didComplete(_ error: Error?) {
        self.error = error
        print("didComplete")
        completion?(data, self.error)
    }
    
    func didReceive(_ data: Data) {
        self.data.append(data)
    }
    
    func awaitCompletion() async throws -> Data {
        try await withUnsafeThrowingContinuation { continuation in
            self.completion = { data, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
        }
    }
}

extension URLSession: UnsafeSendable {}

extension SessionDelegate: URLSessionTaskDelegate {
//    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
//        await self.owner?.request(for: task)?.didComplete(error)
//    }
}

extension SessionDelegate: URLSessionDataDelegate {
//    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
//        await owner?.request(for: dataTask)?.didReceive(data)
//    }
}

I'm also having a lot of trouble getting the initializers I want to work, but that's another issue.

Hmm. Your Session is conceptually an actor, but I'm not sure you can reasonably use a language-supported actor for this with the current implementation because of the complex interaction with URLSession's use of queues. Custom executors should make this reasonable to model directly as an actor, although I think we might need some way way to tell the system that it can assume you're running on a particular executor already. In the meantime, you can use a queue internally and present an async interface externally, which isn't very satisfying technically but does do the job of making this fit into a program that's broadly using async/await. I think your example isn't doing anything that you couldn't have done with the completion-handler interface instead of the more general delegate, though, so maybe there's a gap with the real-world example here.

Right. This example is just an experiment towards the beginning of rebuilding Alamofire using the new concurrency model. The current version already uses serial queues internally to manage async state updates and locks to manage synchronous functions and mutable state. I'm trying to gain insight into how the concurrency features work by applying them to the concurrency domain I've already built. I also have an experiment offering async handlers instead of completion handlers, this is just the next step.

It's funny you should mention the URLSession queuing, as I brought that example up several times in previous pitches. Eventually I got an answer that the queue shouldn't really matter as there may simply be a performance impact due to queue hopping between the URLSession's delegateQueue and the various executors. Are there considerations beyond that?

Frankly, while my current model does use the same underlying DispatchQueue as both my Session's shared rootQueue and the URLSession's delegateQueue, as that avoids a whole lot of async calls between the two, it does seem like a better idea to allow the URLSession's queue to operate independently so throughput isn't affected by work being performed by the requests. Replacing my existing serial DispatchQueue with the built in concurrency seems like a good idea, no?

So I'd still be very interested in a model that allows me make an actor a URLSessionDelegate, as that seems like something that needs to be possible no matter what. I took the compiler's advice and marked the delegate methods nonisolated and used detach to call my existing await calls. Is that roughly equivalent to what @asyncHandler did?

I think that might be exactly what the old @asyncHandler did.

The existing URLSessionDelegate design is essentially an "actor protocol" — all the requirements are invoked on a consistent executor, namely, the operation queue you construct the session with — except that there's really no way for the compiler to know and automate that. Probably the most direct port would be to make an initializer of URLSession that takes an instance of an actor protocol corresponding to everything in the URLSessionDelegate hierarchy, and then make an ObjC adapter which sits between them. The big problem with that approach, of course, is that URLSessionDelegate and its derived protocols trade heavily on optional requirements, which aren't supported in a non-@objc protocol, which an actor protocol would have to be.

1 Like

How? I couldn't even find build settings anymore, I assume it's only available for .xcodeproj project, not SPM one.