2

I want to create a URL request and pass it into an async let binding, which seems natural to me:

func test() async {
    // Force unwraps (!) are just for demo
    var request = URLRequest(url: URL(string:"https://stackoverflow.com")!)
    request.httpMethod = "GET" // just for example
    // some more tinkering with `request` here.
    
    //  Error on this line: "Reference to captured var 'request' in concurrently-executing code"
    async let responseData = URLSession.shared.data(for: request).0
    
    // It works like this:
    // let immutableRequest = request
    // async let responseData = URLSession.shared.data(for: immutableRequest).0
    
    // other stuff
    print("Response body: \(String(data: try! await responseData, encoding: .utf8))")
}

Why do I get an error? URLRequest is a struct, so when we pass it into a function, the function should get a copy of that struct, so if I modify request after the async call, it shouldn't affect the call.

I know that the call happens asynchronously, but I would expect it to capture the parameters at the point of the call and then continue execution as though the call has been made (so, a copy of request at the point of the call has been passed into data(for: request).

Also, is there a convenient way to do it without creating another let variable and without using a closure to initialize request, like:

let request: URLRequest = {
    var result = URLRequest(url: URL(string:"https://stackoverflow.com")!)
    result.httpMethod = "GET"
    return result
}()
FreeNickname
  • 7,398
  • 2
  • 30
  • 60

1 Answers1

3

As SE-0317 - async let bindings says:

... async let is similar to a let, in that it defines a local constant that is initialized by the expression on the right-hand side of the =. However, it differs in that the initializer expression is evaluated in a separate, concurrently-executing child task.

The child task begins running as soon as the async let is encountered.

...

A async let creates a child-task, which inherits its parent task's priority as well as task-local values. Semantically, this is equivalent to creating a one-off TaskGroup which spawns a single task and returns its result ...

Similarly to the [group.addTask] function, the closure is @Sendable and nonisolated, meaning that it cannot access non-sendable state of the enclosing context. For example, it will result in a compile-time error, preventing a potential race condition, for a async let initializer to attempt mutating a closed-over variable:

var localText: [String] = ...
async let w = localText.removeLast() // error: mutation of captured var 'localText' in concurrently-executing code

The async let initializer may refer to any sendable state, same as any non-isolated sendable closure.

So, it is not the case that the parameter to data(for:delegate:) is copied and then the asynchronous task is created, but rather the other way around.

Usually, if you were using a closure, you would just add request to the closure’s capture list, but that’s not possible in this case. E.g., you could create a Task yourself with a capture list, achieving something akin to async let, but with greater control:

func test() async throws {
    var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
    request.httpMethod = "GET" // just for example

    let task = Task { [request] in
        try await URLSession.shared.data(for: request).0
    }

    // do some more stuff in parallel

    print("Response body: \(String(data: try await task.value, encoding: .utf8) ?? "Not string")")
}

Obviously, you can simply await the data(for:delegate:), rather than async let, and the problem goes away:

func test() async throws {
    var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
    request.httpMethod = "GET" // just for example

    let data = try await URLSession.shared.data(for: request).0

    print("Response body: \(String(data: data, encoding: .utf8) ?? "Not string")")
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thank you! I completely forgot about 'Sendable' – FreeNickname Jan 25 '22 at 04:56
  • I was wondering why await is ok in this case. I asked that ChatGPT and got this response: "By using await to wait for the completion of async function before accessing its result, you ensure that the code in test() is executed sequentially and that there are no concurrent accesses to request variable. This eliminates the risk of race conditions and unexpected behavior." – Ruslan Mansurov Feb 11 '23 at 00:59
  • That’s a good example of why one should be cautious about relying on ChatGPT. It sounds so convincing and authoritative, but in this case, at least, it misses the mark (e.g., it is the use of `Sendable` types across actor boundaries that eliminates the risk of race conditions). And it fails to provide references, so it is really hard to determine the validity of its statements. See the [ChatGPT policy](https://stackoverflow.com/help/gpt-policy), which explains why ChatGPT answers are currently not accepted on this forum. – Rob Feb 11 '23 at 01:40