[Concurrency] modifying beginAsync, suspendAsync to support cancellation

Hi,

to support cancellation, I propose the following changes to `beginAsync()` and `suspendAsync()`:

`beginAsync()` returns an object adhering to a `Cancelable` protocol:

func beginAsync(_ body: () async throws-> Void) rethrows -> Cancelable

protocol Cancelable { func cancel() }

`suspendAsync()` takes a new thunk parameter:

func suspendAsync<T>(onCancel: () -> Void, body: (cont: (T) -> Void, err: (Error) -> Void) async -> T 

Now, when `cancel()` is invoked, the `onCancel` thunk in the current suspension (if any) will be called.

Example:

var task: Cancelable?

@IBAction func buttonDidClick(sender: AnyObject) {
  task = beginAsync {
    do {
      let image = try await processImage()
      imageView.image = image 
    } catch AsyncError.canceled {
      imageView.image = nil // or some fallback image...
    } catch {
      // other handling
    }
  }  
)

@IBAction func cancelDidClick(sender: AnyObject) {
  task?.cancel()
}

func processImage() async throws -> UIImage {
  // This processing should be on a background queue (or better an Actor :-) - but ignored for this example
  var cancelled = false
  suspendAsync(onCancel: {
    cancelled = true
  }, body: { cont, err in
     while !done && !cancelled {
       // do the processing on image until done or canceled
     }
     guard !cancelled else { err(AsyncError.canceled) } // BTW, maybe change signature of `suspendAsync` to allow to throw here instead
     cont(image)
  }
}

Cheers
Marc

Hi,

to support cancellation, I propose the following changes to `beginAsync()` and `suspendAsync()`:

`beginAsync()` returns an object adhering to a `Cancelable` protocol:

func beginAsync(_ body: () async throws-> Void) rethrows -> Cancelable

protocol Cancelable { func cancel() }

`suspendAsync()` takes a new thunk parameter:

func suspendAsync<T>(onCancel: () -> Void, body: (cont: (T) -> Void, err: (Error) -> Void) async -> T 

Now, when `cancel()` is invoked, the `onCancel` thunk in the current suspension (if any) will be called.

Example:

var task: Cancelable?

@IBAction func buttonDidClick(sender: AnyObject) {
 task = beginAsync {
   do {
     let image = try await processImage()
     imageView.image = image 
   } catch AsyncError.canceled {
     imageView.image = nil // or some fallback image...
   } catch {
     // other handling
   }
 }  
)

@IBAction func cancelDidClick(sender: AnyObject) {
 task?.cancel()
}

Just adding here that instead of directly using the low-level beginAsync, a Future/Promise could be used instead:

var task: Future&lt;UIImage&gt;?

@IBAction func buttonDidClick\(sender: AnyObject\) \{
&nbsp;&nbsp;task = Future \{
&nbsp;&nbsp;&nbsp;&nbsp;try await processImage\(\)
&nbsp;&nbsp;\}
&nbsp;&nbsp;do \{
&nbsp;&nbsp;&nbsp;&nbsp;imageView\.image = try await task\!\.get\(\)
&nbsp;&nbsp;\} catch AsyncError\.canceled \{
&nbsp;&nbsp;&nbsp;&nbsp;imageView\.image = nil // or some fallback image\.\.\.
&nbsp;&nbsp;\} catch \{
&nbsp;&nbsp;&nbsp;&nbsp;// other handling
&nbsp;&nbsp;\}
\}

@iBAction func cancelDidClick\(sender: AnyObject\) \{
&nbsp;&nbsp;task?\.cancel\(\)
\}

Of course, the init of Future would have to be changed

convenience init(_ body: () throws async -> T) {
self.init()
task = beginAsync {
do {
self.fulfill(try await body())
} catch {
self.fail(error)
}
}
}
(BTW also added missing throws and try in code above)

and cancel() would have to be added to Future:

public func cancel\(\) \{
&nbsp;&nbsp;task?\.cancel\(\)
\}

func processImage() async throws -> UIImage {
// This processing should be on a background queue (or better an Actor :slight_smile: - but ignored for this example
var cancelled = false
suspendAsync(onCancel: {

cancelled = true
}, body: { cont, err in
while !done && !cancelled {
// do the processing on image until done or canceled
}
guard !cancelled else { err(AsyncError.canceled) } // BTW, maybe change signature of suspendAsync to allow to throw here instead
cont(image)
}
}

^ BTW, this should be `return await suspendAsync(…`

···

Am 19.08.2017 um 20:33 schrieb Marc Schlichte via swift-evolution <swift-evolution@swift.org>:

Cheers
Marc

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I think this makes the proposed async/await coroutines more viable.
Building on this, if the proposed Cancelable became:

    protocol ExecutionControl {
        /// Causes the executing coroutine to throw
`TerminateCoroutine.cancelled` and also terminates all the sub-coroutines.
        var isCancelled: Bool { set }

        /// If an await exceeds timeout then executing task throws
`TerminateCoroutine.timeout` and also terminates all the sub-coroutines.
        var timeout: DispatchTimeInterval { get set }
    }

Then async/await coroutine would have feature parity with a typical
`Future` - which would be good.

PS Effectively the execution service is returning the `Future`!

  -- Howard.

···

On 29 August 2017 at 09:42, Marc Schlichte via swift-evolution < swift-evolution@swift.org> wrote:

Am 19.08.2017 um 20:33 schrieb Marc Schlichte via swift-evolution < > swift-evolution@swift.org>:

Hi,

to support cancellation, I propose the following changes to `beginAsync()`
and `suspendAsync()`:

`beginAsync()` returns an object adhering to a `Cancelable` protocol:

func beginAsync(_ body: () async throws-> Void) rethrows -> Cancelable

protocol Cancelable { func cancel() }

`suspendAsync()` takes a new thunk parameter:

func suspendAsync<T>(onCancel: () -> Void, body: (cont: (T) -> Void, err:
(Error) -> Void) async -> T

Now, when `cancel()` is invoked, the `onCancel` thunk in the current
suspension (if any) will be called.

Example:

var task: Cancelable?

@IBAction func buttonDidClick(sender: AnyObject) {
 task = beginAsync {
   do {
     let image = try await processImage()
     imageView.image = image
   } catch AsyncError.canceled {
     imageView.image = nil // or some fallback image...
   } catch {
     // other handling
   }
 }
)

@IBAction func cancelDidClick(sender: AnyObject) {
 task?.cancel()
}

Just adding here that instead of directly using the low-level
`beginAsync`, a Future/Promise could be used instead:

var task: Future<UIImage>?

@IBAction func buttonDidClick(sender: AnyObject) {
  task = Future {
    try await processImage()
  }
  do {
    imageView.image = try await task!.get()
  } catch AsyncError.canceled {
    imageView.image = nil // or some fallback image...
  } catch {
    // other handling
  }
}

@iBAction func cancelDidClick(sender: AnyObject) {
  task?.cancel()
}


Of course, the init of Future would have to be changed

  convenience init(_ body: () throws async -> T) {
    self.init()
    task = beginAsync {
      do {
        self.fulfill(try await body())
      } catch {
        self.fail(error)
      }
    }
  }

(BTW also added missing throws and try in code above)

and `cancel()` would have to be added to `Future`:

public func cancel() {
  task?.cancel()
}


func processImage() async throws -> UIImage {
 // This processing should be on a background queue (or better an Actor
:-) - but ignored for this example
 var cancelled = false
 suspendAsync(onCancel: {

   cancelled = true
 }, body: { cont, err in
    while !done && !cancelled {
      // do the processing on image until done or canceled
    }
    guard !cancelled else { err(AsyncError.canceled) } // BTW, maybe
change signature of `suspendAsync` to allow to throw here instead
    cont(image)
 }
}

^ BTW, this should be `return await suspendAsync(…`

Cheers
Marc

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I dont like the idea of beginAsync, suspendAsync be a standard library
functions to support a language level resource.

beginAsync can be only `async` and suspendAsync can be "await resync"
(something like that).

Saing that, cancel a operations is not that simple! Abrupt abort any thread
can leave some consequences. Usually, for network code is ok, but some
other multi-thread operations is not (like calculus, drawing, file
operations, etc)

Timeout is not useful on mathematical operations, abort any thread which is
accessing a physical device also can be dangerous (I broke one machine one
time). There's no straight answer to "how" we can stop a execution block.
Pause/Cancel when using a GCD queue is not that simple too! We need to
remember that one of Swift's goals is to be a systems language. Not
everything that is useful in "high level" is useful in "low level".

Yes, coordination is important, but not a simple question to a run-time
agnostic way to implement concurrency. Of course we can choose to make a
run-time specific way to implement this, but i don't think is a valuable
choice now.

But.... return something beginAsync, suspendAsync, ever a `bool` is a good
ideia (the proposal cover that too).

···

Em seg, 28 de ago de 2017 às 22:42, Howard Lovatt via swift-evolution < swift-evolution@swift.org> escreveu:

I think this makes the proposed async/await coroutines more viable.
Building on this, if the proposed Cancelable became:

    protocol ExecutionControl {
        /// Causes the executing coroutine to throw
`TerminateCoroutine.cancelled` and also terminates all the sub-coroutines.
        var isCancelled: Bool { set }

        /// If an await exceeds timeout then executing task throws
`TerminateCoroutine.timeout` and also terminates all the sub-coroutines.
        var timeout: DispatchTimeInterval { get set }
    }

Then async/await coroutine would have feature parity with a typical
`Future` - which would be good.

PS Effectively the execution service is returning the `Future`!

  -- Howard.

On 29 August 2017 at 09:42, Marc Schlichte via swift-evolution < > swift-evolution@swift.org> wrote:

Am 19.08.2017 um 20:33 schrieb Marc Schlichte via swift-evolution < >> swift-evolution@swift.org>:

Hi,

to support cancellation, I propose the following changes to
`beginAsync()` and `suspendAsync()`:

`beginAsync()` returns an object adhering to a `Cancelable` protocol:

func beginAsync(_ body: () async throws-> Void) rethrows -> Cancelable

protocol Cancelable { func cancel() }

`suspendAsync()` takes a new thunk parameter:

func suspendAsync<T>(onCancel: () -> Void, body: (cont: (T) -> Void, err:
(Error) -> Void) async -> T

Now, when `cancel()` is invoked, the `onCancel` thunk in the current
suspension (if any) will be called.

Example:

var task: Cancelable?

@IBAction func buttonDidClick(sender: AnyObject) {
 task = beginAsync {
   do {
     let image = try await processImage()
     imageView.image = image
   } catch AsyncError.canceled {
     imageView.image = nil // or some fallback image...
   } catch {
     // other handling
   }
 }
)

@IBAction func cancelDidClick(sender: AnyObject) {
 task?.cancel()
}

Just adding here that instead of directly using the low-level
`beginAsync`, a Future/Promise could be used instead:

var task: Future<UIImage>?

@IBAction func buttonDidClick(sender: AnyObject) {
  task = Future {
    try await processImage()
  }
  do {
    imageView.image = try await task!.get()
  } catch AsyncError.canceled {
    imageView.image = nil // or some fallback image...
  } catch {
    // other handling
  }
}

@iBAction func cancelDidClick(sender: AnyObject) {
  task?.cancel()
}


Of course, the init of Future would have to be changed

  convenience init(_ body: () throws async -> T) {
    self.init()
    task = beginAsync {
      do {
        self.fulfill(try await body())
      } catch {
        self.fail(error)
      }
    }
  }

(BTW also added missing throws and try in code above)

and `cancel()` would have to be added to `Future`:

public func cancel() {
  task?.cancel()
}


func processImage() async throws -> UIImage {
 // This processing should be on a background queue (or better an Actor
:-) - but ignored for this example
 var cancelled = false
 suspendAsync(onCancel: {

   cancelled = true
 }, body: { cont, err in
    while !done && !cancelled {
      // do the processing on image until done or canceled
    }
    guard !cancelled else { err(AsyncError.canceled) } // BTW, maybe
change signature of `suspendAsync` to allow to throw here instead
    cont(image)
 }
}

^ BTW, this should be `return await suspendAsync(…`

Cheers
Marc

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution