My experience with Concurrency

@crontab That was spot on. Thanks.
If it helps @ibex10, I can provide more concrete examples.

Case 1

Assume that I am writing library that encapsulates the communication to a Bluetooth peripheral. As I've outlined before, the requirements are that before I send subsequent requests I need to wait for an acknowledgement from the peripheral.

func enqueue(_ data: Data) async throws {
  // This is synchronous but theoretically I could even use .withResponse and wait for a callback from peripheral(_:didWriteValueFor:error:)
  peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
  // However in this scenario I am just waiting for peripheral(_:didUpdateValueFor:error:) which I converted to an AsyncStream
  for try await response in responseStream {
    if /* is correct acknowledgement for request */ {
      break
    }
  }
}

I now need to ensure, that if enqueue is called multiple times from multiple tasks, it still waits for the previous acknowledgement before the next peripheral.writeValue(_:for:type:).

I solved this with AsyncSemaphore by @gwendal.roue by doing this:

let bluetoothSemaphore = AsyncSemaphore(value: 1)

func enqueue(_ data: Data) async throws {

  await bluetoothSemaphore.wait()
  defer { bluetoothSemaphore.signal() }

  peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
  for try await response in responseStream {
    if /* is correct acknowledgement for request */ {
      break
    }
  }
}

Case 2

Kind of a similar use case now that I am thinking about it. However this time, it was a device I was communicating with over local network. This time the requirement was to have a cooldown between sending commands.

func enqueue(_ data: Data) async throws {
  // This is synchronous but theoretically I could even use .contentProcessed and wait for its callback
  connection.send(content: data, completion: .idempotent)
  // Let the remote device cooldown by waiting here
  try await Task.sleep(...)
}

Same solution here with AsyncSemaphore.

Conclusion

Again, I am not saying that this kind of API requires primitives like semaphores. I do see and agree with the point of @ktoso being made here Using semaphores in actor code - #40 by ktoso. And I do think this can be solved with actors, if they would support some kind of non-reentrancy.

At the time of implementing those features the best solution was (and I still think it is) AsyncSemaphore.

1 Like