<div dir="ltr">Using a Future library, see below, you can do what you want. In particular a future that is cancellable is more powerful that the proposed async/await. Here is an extended example of a typical UI task including users cancelling tasks that is written using a future library (see below):<div><br></div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"> <font face="monospace, monospace">@IBOutlet weak var timeButtonWasPressed: NSTextField!<br> <br> static func progress(_ window: NSWindow, _ progress: NSProgressIndicator) -> Future<String> {<br> return AsynchronousFuture(timeout: .seconds(3)) { isCancelled -> String in // Timeout set to 3 seconds to ensure normal progress closes the window before timeout does.<br> defer { // Make sure the window always closes.<br> Thread.executeOnMain {<br> window.close()<br> }<br> }<br> var isFinished = false<br> var isClosed = false<br> while !(isFinished || isClosed || isCancelled()) {<br> Thread.sleep(forTimeInterval: 0.1) // Would do real work here!<br> Thread.executeOnMain {<br> guard window.isVisible else { // Check if user has closed the window.<br> isClosed = true<br> return<br> }<br> progress.increment(by: 1)<br> if progress.doubleValue >= progress.maxValue { // Check if work done.<br> isFinished = true<br> }<br> }<br> }<br> if isClosed || isCancelled() { // Cancelled by user closing window, by call to `cancel`, or by timeout<br> throw CancelFuture.cancelled<br> }<br> return "\(DispatchTime.now().uptimeNanoseconds)"<br> }<br> }<br> <br> var windowOrigin = NSPoint(x: 0, y: NSScreen.main?.visibleFrame.height ?? 0) // First window in top left of screen.<br> <br> @IBAction func buttonPushed(_ _: NSButton) {<br> let progressFrame = NSRect(x: 10, y: 10, width: 200, height: 100) // On main thread; pop up a progress window.<br> let progress = NSProgressIndicator(frame: progressFrame)<br> progress.minValue = 0<br> progress.maxValue = 20<br> progress.isIndeterminate = false<br> let windowFrame = NSRect(x: 0, y: 0, width: progressFrame.width + 20, height: progressFrame.height + 20)<br> let window = NSWindow(contentRect: windowFrame, styleMask: [.titled, .closable], backing: .buffered, defer: false)<br> window.contentView?.addSubview(progress)<br> windowOrigin = window.cascadeTopLeft(from: windowOrigin) // Position window.<br> window.isReleasedWhenClosed = false // Needed to keep ARC happy!<br> window.orderFront(self) // Display the window but don't give it the focus.<br> let _ = AsynchronousFuture { _ -> Void in // Runs on global default queue.<br> let creationTime = ViewController.progress(window, progress).get ?? "Cancelled" // Progress bar with close to allow user cancellation and finishes automatically after 2 seconds to allow cancellation or many button presses to pop up other progress windows.<br> Thread.executeOnMain {<br> self.timeButtonWasPressed.stringValue = creationTime<br> }<br> }<br> }<br></font><br></blockquote>The above pops up a window with a progress bar in it every time the user hits a button (buttonPushed), once the progress bar has completed or is cancelled the main UI is updated, including noting cancellation by the user. It can popup multiple windows and cancel them in any order. <div><br></div><div>This is easier to do with a future library than the proposed async/await because the library has concepts of: cancellation, timeout, and control over which queue routines run on.</div><div><br></div><div>Future library below:</div><div><br></div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><font face="monospace, monospace">//<br>// main.swift<br>// Future<br>// Version 0.1<br>//<br>// Created by Howard Lovatt on 22/8/17.<br>// Copyright © 2017 Howard Lovatt.<br>// This work is licensed under a Creative Commons Attribution 4.0 International License, <a href="http://creativecommons.org/licenses/by/4.0/">http://creativecommons.org/licenses/by/4.0/</a>.<br>//<br><br>import Foundation<br><br>/// - note:<br>/// - Written in GCD but execution service would be abstracted for a 'real' version of this proposed `Future`.<br>/// - It might be necessary to write an atomic class/struct and use it for _status and isCancelled in CalculatingFuture; see comments after property declarations.<br>/// - If _status and isCancelled in CalculatingFuture where atomic then future would be thread safe.<br><br>extension Thread {<br> /// Run the given closure on the main thread (thread hops to main) and *wait* for it to complete before returning its value; useful for updating and reading UI components.<br> /// Checks to see if already executing on the main thread and if so does not change to main thread before executing closure, since changing to main when already on main would cause a deadlock.<br> /// - note: Not unique to `Future`, hence an extension on `Thread`.<br> static func executeOnMain<T>(closure: @escaping () -> T) -> T {<br> var result: T?<br> if Thread.isMainThread {<br> result = closure()<br> } else {<br> DispatchQueue.main.sync {<br> result = closure()<br> }<br> }<br> return result!<br> }<br>}<br><br>/// All possible states for a `Future`; a future is in exactly one of these.<br>enum FutureStatus<T> {<br> /// Currently running or waiting to run; has not completed, was not cancelled, has not timed out, and has not thrown.<br> case running<br> <br> <br> /// Ran to completion; was not cancelled, did not timeout, and did not throw, no longer running.<br> case completed(result: T)<br> <br> <br> /// Was cancelled, timed out, or calculation threw an exception; no longer running.<br> case threw(error: Error)<br>}<br><br>/// An error that signals the future was cancelled.<br>enum CancelFuture: Error {<br> /// Should be thrown by a future's calculation when requested to do so via its `isCancelled` argument (which arises if the future is cancelled or if the future times out).<br> case cancelled<br>}<br><br>/// Base class for futures; acts like a future that was cancelled, i.e. no result and threw `CancelFuture.cancelled`.<br>/// - note:<br>/// - You would normally program to `Future`, not one of its derived classes, i.e. arguments, return types, properties, etc. typed as `Future`.<br>/// - Futures are **not** thread safe; i.e. they cannot be shared between threads though their results can and they themselves can be inside any single thread.<br>/// - This class is useful in its own right; not just a base class, but as a future that is known to be cancelled.<br>class Future<T> {<br> /// The current state of execution of the future.<br> /// - note:<br> /// - The status is updated when the future's calculation finishes; therefore there will be a lag between a cancellation or a timeout and status reflecting this.<br> /// - This status lag is due to the underlying thread system provided by the operating system that typically does not allow a running thread to be terminated.<br> /// - Because status can lag cancel and timeout; prefer get over status, for obtaining the result of a future and if detailed reasons for a failure are not required.<br> /// - Status however offers detailed information if a thread terminates by throwing (including cancellation and time out) and is therefore very useful for debugging.<br> /// - note: In the case of this base class, always cancelled; returns `.threw(error: CancelFuture.cancelled)`.<br> var status: FutureStatus<T> {<br> return .threw(error: CancelFuture.cancelled)<br> }<br> <br> <br> /// Wait until the value of the future is calculated and return it; if future timed out, if future was cancelled, or if calculation threw, then return nil.<br> /// The intended use of this property is to chain with the nil coalescing operator, `??`, to provide a default, a retry, or an error message in the case of failure.<br> /// - note:<br> /// - Timeout is only checked when `get` is called.<br> /// - If a future is cancelled or times out then get will subsequently return nil; however it might take some time before status reflects this calculation because status is only updated when the calculation stops.<br> /// - note: In the case of this base class, always return nil.<br> var get: T? {<br> return nil<br> }<br> <br> <br> /// Cancel the calculation of the future; if it has not already completed.<br> /// - note:<br> /// - Cancellation should cause `CancelFuture.cancelled` to be thrown and hence the future's status changes to `threw` ('should' because the calculation can ignore its `isCancelled` argument or throw some other error).<br> /// - `isCancelled` is automatically checked on entry and exit to the calculation and therefore status will update before and after execution even if the calculation ignores its argument.<br> /// - Cancellation will not be instantaneous and therefore the future's status will not update immediately; it updates when the calculation terminates (either by returning a value or via a throw).<br> /// - If a future timeouts, it cancels its calculation.<br> /// - If the future's calculation respects its `isCancelled` argument then a timeout will break a deadlock.<br> /// - If a future is cancelled by either cancel or a timeout, subsequent calls to `get` will return nil; even if the calculation is still running and hence status has not updated.<br> /// - note: In the case of this base class, cancel does nothing since this future is always cancelled.<br> func cancel() {}<br>}<br><br>/// A future that calculates its value on the given queue asynchronously (i.e. its init method returns before the calculation is complete) and has the given timeout to bound the wait time when `get` is called.<br>final class AsynchronousFuture<T>: Future<T> {<br> private var _status = FutureStatus<T>.running // Really like to mark this volatile and atomic (it is written in background thread and read in foreground)!<br> <br> <br> override var status: FutureStatus<T> {<br> return _status<br> }<br> <br> <br> private let group = DispatchGroup()<br> <br> <br> private let timeoutTime: DispatchTime<br> <br> <br> private var isCancelled = false // Really like to mark this volatile (it is a bool so presumably atomic, but it is set in forground thread and read in background)!<br> <br> <br> /// - note: The default queue is the global queue with default quality of service.<br> /// - note:<br> /// Regarding the `timeout` argument:<br> /// - Timeout starts from when the future is created, not when `get` is called.<br> /// - The time used for a timeout is processor time; i.e. it excludes time when the computer is in sleep mode.<br> /// - The default timeout is 1 second.<br> /// - If the calculation times out then the calculation is cancelled.<br> /// - The timeout is only checked when `get` is called; i.e. the calculation will continue for longer than timeout, potentially indefinitely, if `get` is not called.<br> /// - Also see warning below.<br> /// - warning:<br> /// Be **very** careful about setting long timeouts; if a deadlock occurs it is diagnosed/broken by a timeout occurring!<br> /// If the calculating method respects its `isCancelled` argument a timeout will break a deadlock, otherwise it will only detect a deadlock.<br> init(queue: DispatchQueue = .global(), timeout: DispatchTimeInterval = .seconds(1), calculation: @escaping (_ isCancelled: () -> Bool) -> FutureStatus<T>) {<br> self.timeoutTime = DispatchTime.now() + timeout<br> super.init() // Have to complete initialization before result can be calculated.<br> queue.async { // Deliberately holds a strong reference to self, so that a future can be side effecting.<br> self.group.enter()<br> defer {<br> self.group.leave()<br> }<br> if self.isCancelled { // Future was cancelled before execution began.<br> self._status = .threw(error: CancelFuture.cancelled)<br> return<br> }<br> self._status = calculation {<br> self.isCancelled // Pass `isCancelled` to `calculation` (via a closure so that it isn't copied and therefore reflects its current value).<br> }<br> if self.isCancelled { // Future was cancelled during execution.<br> self._status = .threw(error: CancelFuture.cancelled)<br> }<br> }<br> }<br> <br> <br> /// See above `init` for description.<br> /// This `init` accepts a closure that returns a `T`; the above `init`'s closure returns a `FutureStatus<T>`.<br> /// This `init`'s closure is wrapped to return a `FutureStatus<T>` and this `init` calls the above `init`.<br> convenience init(queue: DispatchQueue = .global(), timeout: DispatchTimeInterval = .seconds(1), calculation: @escaping (_ isCancelled: () -> Bool) throws -> T) {<br> self.init(queue: queue, timeout: timeout) { isCancelled -> FutureStatus<T> in<br> var resultOrError: FutureStatus<T><br> do {<br> resultOrError = .completed(result: try calculation(isCancelled))<br> } catch {<br> resultOrError = .threw(error: error)<br> }<br> return resultOrError<br> }<br> }<br> <br> <br> /// See `init` 2 above for description.<br> /// This `init` accepts a closure that accepts no arguments, unlike the closures for the other `init`s that accept `isCancelled`, and returns a `(T?, Error?)`; the `init`' 2 above's closure returns a `FutureStatus<T>`.<br> /// This `init`'s closure is wrapped to return a `FutureStatus<T>` and this `init` calls the `init` 2 above.<br> convenience init(queue: DispatchQueue = .global(), timeout: DispatchTimeInterval = .seconds(1), calculation: @escaping () -> (T?, Error?)) {<br> self.init(queue: queue, timeout: timeout) { _ -> FutureStatus<T> in<br> var resultOrError: FutureStatus<T><br> let (result, error) = calculation()<br> if error == nil {<br> resultOrError = .completed(result: result!)<br> } else {<br> resultOrError = .threw(error: error!)<br> }<br> return resultOrError<br> }<br> }<br> <br> <br> override var get: T? {<br> guard !isCancelled else { // Catch waiting for a cancel to actually happen.<br> return nil<br> }<br> while true { // Loop until not running, so that after a successful wait the result can be obtained.<br> switch _status {<br> case .running:<br> switch group.wait(timeout: timeoutTime) { // Wait for calculation completion.<br> case .success:<br> break // Loop round and test status again to extract result<br> case .timedOut:<br> isCancelled = true<br> return nil<br> }<br> case .completed(let result):<br> return result<br> case .threw(_):<br> return nil<br> }<br> }<br> }<br> <br> <br> override func cancel() {<br> switch _status {<br> case .running:<br> isCancelled = true<br> case .completed(_):<br> return // Cannot cancel a completed future.<br> case .threw(_):<br> return // Cannot cancel a future that has timed out, been cancelled, or thrown.<br> }<br> }<br>}<br><br>/// A future that doesn't need calculating, because the result is already known.<br>final class KnownFuture<T>: Future<T> {<br> private let result: T<br> <br> <br> override var status: FutureStatus<T> {<br> return .completed(result: result)<br> }<br> <br> <br> init(_ result: T) {<br> self.result = result<br> }<br> <br> <br> override var get: T? {<br> return result<br> }<br>}<br><br>/// A future that doesn't need calculating, because it is known to fail.<br>final class FailedFuture<T>: Future<T> {<br> private let _status: FutureStatus<T><br> <br> <br> override var status: FutureStatus<T> {<br> return _status<br> }<br> <br> <br> init(_ error: Error) {<br> _status = .threw(error: error)<br> }<br>}</font></blockquote></div><div class="gmail_extra"><br clear="all"><div><div class="gmail_signature" data-smartmail="gmail_signature"> -- Howard.<br></div></div>
<br><div class="gmail_quote">On 24 August 2017 at 14:26, Maxim Veksler via swift-evolution <span dir="ltr"><<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div dir="rtl"><div dir="ltr">I think that the solution you are describing is how RxSwift (ReactiveX) solves this problem. </div><div dir="ltr"><br></div><div dir="ltr">I believe Rx, like many other higher level abstractions would benefit from async, actors behind the scenes, as an implementation detail.</div></div><br><div class="gmail_quote"><div dir="rtl">בתאריך יום ד׳, 23 באוג׳ 2017 ב-20:41 מאת Joe Groff via swift-evolution <<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a>><wbr>:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div style="word-wrap:break-word"><br><div><blockquote type="cite"><div>On Aug 19, 2017, at 4:56 AM, Jakob Egger via swift-evolution <<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a>> wrote:</div><br class="m_-9015920074462218573m_-1773459520748767038Apple-interchange-newline"><div><div style="word-wrap:break-word">I've read async/await proposal, and I'm thrilled by the possibilities. Here's what I consider the canonical example:<div><pre><code>@IBAction func buttonDidClick(sender:<wbr>AnyObject) {
beginAsync {
let image = await processImage()
imageView.image = image
}
}</code></pre></div><div>This is exactly the kind of thing I will use async/await for!</div><div><br></div><div>But while this example looks extremely elegant, it would suffer from a number of problems in practice:</div><div><br></div><div>1. There is no guarantee that you are on the main thread after `await processImage()`<br>2. There is no way to cancel processing <br>3. Race Condition: If you click the button a second time before `processImage()` is done, two copies will run simultaneously and you don't know which image will "win".<br><br></div><div>So I wondered: What would a more thorough example look like in practice? How would I fix all these issues?</div><div><br></div><div>After some consideration, I came up with the following minimal example that addresses all these issues:</div><div><pre><code>class ImageProcessingTask {
var cancelled = false
func process() async -> Image? { … }
}
</code></pre><pre><code>var currentTask: ImageProcessingTask?
@IBAction func buttonDidClick(sender:<wbr>AnyObject) {
<div> currentTask?.cancelled = true</div><div> let task = ImageProcessingTask()</div><div> currentTask = task</div> beginAsync {
guard let image = await task.process() else { return }
DispatchQueue.main.async {
guard task.cancelled == false else { return }
imageView.image = image
}
}
}</code></pre><div>If my example isn't obvious, I've documented my thinking (and some alternatives) in a gist:</div></div><div><a href="https://gist.github.com/jakob/22c9725caac5125c1273ece93cc2e1e7" target="_blank">https://gist.github.com/jakob/<wbr>22c9725caac5125c1273ece93cc2e1<wbr>e7</a></div><div><br></div><div><div>Anyway, this more realistic code sample doesn't look nearly as nice any more, and I actually think this could be implemented nicer without async/await:</div></div><div><br></div><div><div><font face="monospace"><span style="white-space:pre-wrap">class ImageProcessingTask {</span></font></div><div><font face="monospace"><span style="white-space:pre-wrap"> var cancelled = false</span></font></div><div><font face="monospace"><span style="white-space:pre-wrap"> func process(completionQueue: DispatchQueue, completionHandler: (Image?)->()) { … }</span></font></div><div><font face="monospace"><span style="white-space:pre-wrap">}</span></font></div><div><font face="monospace"><div><span style="white-space:pre-wrap">@IBAction func buttonDidClick(sender:<wbr>AnyObject) {</span></div><span style="white-space:pre-wrap"> currentTask?.cancelled = true<br> let task = ImageProcessingTask()<br></span><div><span style="white-space:pre-wrap"> currentTask = task</span></div><div><span style="white-space:pre-wrap"> task.process(</span><span style="white-space:pre-wrap">completionQueue: </span><span style="white-space:pre-wrap">DispatchQueue.main</span><span style="white-space:pre-wrap">) { (image) in</span></div></font><font face="monospace"><span style="white-space:pre-wrap"> guard let </span></font><span style="font-family:monospace;white-space:pre-wrap">image = </span><span style="font-family:monospace;white-space:pre-wrap">image else { return }</span></div><div><span style="font-family:monospace;white-space:pre-wrap"> guard task.cancelled == false else { return }</span></div><div><font face="monospace"><span style="white-space:pre-wrap"> </span></font><span style="font-family:monospace;white-space:pre-wrap">imageView.image = image</span><font face="monospace"><span style="white-space:pre-wrap"><br> }<br></span><div><span style="white-space:pre-wrap">}</span></div><span style="white-space:pre-wrap"><br></span></font><div>So I wonder: What's the point of async/await if it doesn't result in nicer code in practice? How can we make async/await more elegant when calling from non-async functions?</div></div></div></div></div></blockquote><br></div></div><div style="word-wrap:break-word"><div>Yeah, it's important to understand that coroutines don't directly offer any form of coordination; they only let you thread execution nicely through existing coordination mechanisms. IBActions by themselves don't offer any coordination, so anything more than fire-and-forget is still going to require explicit code. There are some interesting approaches you still might be able to explore to make this kind of thing nicer; for instance, if buttonDidClick didn't directly trigger the task, but instead communicated with a coroutine via synchronous channels in the style of Go, then that coroutine could be responsible for filtering multiple click events, and could also listen for cancellation events. The actor model Chris proposes in his document could conceivably let you wrap up that low-level channel management in a nice OO-looking wrapper.</div></div><div style="word-wrap:break-word"><div><br></div><div>-Joe</div><br></div>______________________________<wbr>_________________<br>
swift-evolution mailing list<br>
<a href="mailto:swift-evolution@swift.org" target="_blank">swift-evolution@swift.org</a><br>
<a href="https://lists.swift.org/mailman/listinfo/swift-evolution" rel="noreferrer" target="_blank">https://lists.swift.org/<wbr>mailman/listinfo/swift-<wbr>evolution</a><br>
</blockquote></div>
<br>______________________________<wbr>_________________<br>
swift-evolution mailing list<br>
<a href="mailto:swift-evolution@swift.org">swift-evolution@swift.org</a><br>
<a href="https://lists.swift.org/mailman/listinfo/swift-evolution" rel="noreferrer" target="_blank">https://lists.swift.org/<wbr>mailman/listinfo/swift-<wbr>evolution</a><br>
<br></blockquote></div><br></div>