[Concurrency] Async/await and maxConcurrentOperationCount

I'm writing an app that performs actions in a web browser automatically on user's behalf. Currently my app uses OperationQueue to perform asynchronous work. I'm trying to rewrite the app to use async/await instead. Note, that it is necessary for my app to limit the number of concurrently executing work items. Otherwise the app may run into server throttling issues.

However, I can't find an equivalent to OperationQueue's maxConcurrentOperationCount parameter in the new concurrency model. Is there a way to limit the number of concurrently running "lanes" of execution without resorting to using OperationQueue / DispatchQueue / NSLock ?

New concurrency uses cooperative thread pool - number of threads is equal to number of CPU Cores. So, while all threads are busy with one task on each of them, other tasks are suspended, and new tasks don't lead to creation of new threads. In other words, maxConcurrentOperationCount is equal to number of CPU Cores and task running is managed automatically.

Is it possible to share more details, for what concrete purpose is maxConcurrentOperationCount used?

I guess I gotta elaborate on my question. Consider this example:

actor WebBrowser {
    func execute(_ script: JSScript, at url: URL) async throws -> String {
        try await loadPage(at: url)
        return try await executeScript(script)
    }

    private func loadPage(at url: URL) async throws { ... }
    private func executeScript(_ script: JSScript) async throws -> String { ... }  
}

The only guarantee modern concurrency provides for actors is that synchronous code (i.e. methods without async in the signature or blocks of synchronous code between await calls in async methods) is never executed concurrently with other synchronous code on the actor. As far as I understand, this prevents low level data access bugs, but does not eliminate logical data races.

In my example I need all calls to execute(_:at:) to be mutually exclusive. Previously I would just put calls to execute on an OperationQueue with maxConcurrentOperationCount set to 1. But it looks like structured concurrency does not provide any means of doing a similar thing. At least not out of the box.

I tried doing something like this:

actor WebBrowser {
    private var currentTask: Task<String, Error>?

    func execute(_ script: JSScript, at url: URL) async throws -> String {
        let existingTask = currentTask
        let newTask = Task {
            _ = await existingTask?.result
            return try await actuallyExecute(script, at: url)
        }
        currentTask = newTask
        return try await newTask.value
    }

    private func actuallyExecute(_ script: JSScript, at url: URL) async throws -> String {
        try await loadPage(at: url)
        return try await executeScript(script)
    }
}

But queuing up work this way makes cancellation at the call site useless, as cancellation is not propagated to the unstructured task, created in execute method.

Swift Actor is guaranteed to execute only one operation at a time. So all calls to func execute(_ script: JSScript, at url: URL) async throws -> String are serial.

Instead of creating Task {} you can use withTaskCancellationHandler(operation: () -> T, onCancel: () -> Void) method.

Example:

final class JSOperationsQueue {
  private let operations: PassthroughSubject<(script: JSScript, url: URL), Never> = .init()
  private let webBrowser = WebBrowser()
  
  init() {}
  
  func start() {
    Task {
      for await (script, url) in operations.values {
        do {
          let result = try await webBrowser.execute(script, at: url)
          // handle result
        } catch {
          // handle error
        }
      }
    }
  }
  
  func enqueueNew(script: JSScript, url: URL) {
    operations.send((script, url))
  }
}

actor WebBrowser {
    func execute(_ script: JSScript, at url: URL) async throws -> String {
        try await loadPage(at: url)
        return try await executeScript(script)
    }

    private func loadPage(at url: URL) async throws {}
    private func executeScript(_ script: JSScript) async throws -> String { "" }
}


  let queue = JSOperationsQueue()
  queue.start()
  queue.enqueueNew(script: script, url: url)