Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutually cancelling Tasks

Here is a little test bed showing two Tasks that start at the same time, and whichever completes first cancels the other:

func startTest() async throws {
    Task { await test() }
    try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
    // comment out this next line to test the timeout
    NotificationCenter.default.post(name: .init("Test"), object: nil)
}

func test() async {
    let notificationTask = Task {
        for await _ in NotificationCenter.default
            .notifications(named: .init("Test"), object: nil)
            .prefix(1) {}
        print("Done waiting for the notification")
    }
    let timeoutTask = Task {
        try await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC)
        notificationTask.cancel()
        print("I timed out and cancelled the notification task")
    }
    await notificationTask.value
    timeoutTask.cancel()
    print("finished!")
}

As you can see, the idea is that either we receive the notification within 5 seconds, in which case the five second timer is cancelled, or we time out in 5 seconds, in which case waiting for the notification is cancelled.

My question is: is there a neater way to express this? I tried various task group and async let formulations but didn't come up with anything. Maybe this is the "right" way but it grates somehow.

like image 463
matt Avatar asked Oct 23 '25 16:10

matt


1 Answers

  1. Make a TaskGroup
  2. Add all your "racing" tasks to it (they can be more than 2!)
  3. Call await tg.next() to wait for the result of the "race winner"
  4. Call tg.cancelAll() to cancel the outstanding tasks
  5. ???
  6. profit!
import Foundation

func startTest() async throws {
    Task { try await test() }
    try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
    // comment out this next line to test the timeout
    NotificationCenter.default.post(name: .init("Test"), object: nil)
}


func test() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        group.addTask { // formerlynotificationTask
            for await _ in NotificationCenter.default
                .notifications(named: .init("Test"), object: nil)
                .prefix(1) {}
            print("Done waiting for the notification")
        }
        
        group.addTask { // formerly "timeoutTask"
            try await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC)
            print("I timed out and cancelled the notification task")
        }
        
        try await group.next() // Wait for the first completed task
        group.cancelAll() // Cancel the rest
    }
    print("finished!")
}

try await startTest()
like image 77
Alexander Avatar answered Oct 26 '25 07:10

Alexander



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!