[swift-evolution] [Concurrency] Async/Await

Howard Lovatt howard.lovatt at gmail.com
Wed Aug 23 20:35:48 CDT 2017


Hi All,

Really glad that concurrency is on the table for Swift 5.

I am not sure if async/await are worth adding, as is, to Swift because it
is just as easy to do with a library function - in the spirit of Swift 5,
see `Future` library code below that you can play with and run.

If a `Future` class was added and the compiler translated
'completion-handler' code to 'future' code then the running example given
in the whitepaper would become (also see the example in the code below):

func loadWebResource(_ path: String) -> Future<Resource> { ... }
func decodeImage(_ dataResource: Resource, _ imageResource: Resource) ->
Future<Image> { ... }
func dewarpAndCleanupImage(_ image: Image) -> Future<Image> { ... }

func processImageData1() -> Future<Image> {
    let dataResource  = loadWebResource("dataprofile.txt") // dataResource
and imageResource run in parallel.
    let imageResource = loadWebResource("imagedata.dat")
    let imageTmp      = decodeImage(dataResource.get ?? Resource(path:
"Default data resource or prompt user"), imageResource.get ??
Resource(path: "Default image resource or prompt user"))
    let imageResult   =  dewarpAndCleanupImage(imageTmp.get ??
Image(dataPath: "Default image or prompt user", imagePath: "Default image
or prompt user"))
    return imageResult
}


Which I would argue is actually better than the proposed async/await code
because:

   1. The code is naturally parallel, in example dataResource and
   imageResource are calculated in parallel.
   2. The code handles errors and deadlocks by providing a default value
   using `??`.
   3. The code can be deadlock free or can help find deadlocks, see code
   below, due to having a timeout and a means of cancelling.
   4. The programmer who writes the creator of the `Future` controls which
   queue the future executes on, I would contend that this is the best choice
   since the programmer knows what limitations are in the code and can change
   queue if the code changes in later versions. This is the same argument as
   encapsulation, which gives more control to the writer of a struct/class and
   less control to the user of the struct/class.

In summary, I don't think async/await carries its weight (pun intended), as
it stands, compared to a library.

But definitely for more Swift concurrency,

 -- Howard.

PS A memory model and atomic that also guaranteed volatile would really
help with parallel programming!

===========================================================

import Foundation

/// - note:
///   - Written in GCD but execution service would be abstracted for a
'real' version of this proposed `Future`.
///   - It might be necessary to write an atomic class/struct and use it
for _status and isCancelled in CalculatingFuture; see comments after
property declarations.
///   - If _status and isCancelled in CalculatingFuture where atomic then
future would be thread safe.

/// All possible states for a `Future`; a future is in exactly one of these.
enum FutureStatus<T> {
    /// Currently running or waiting to run; has not completed, was not
cancelled, has not timed out, and has not thrown.
    case running

    /// Ran to completion; was not cancelled, did not timeout, and did not
throw, no longer running.
    case completed(result: T)

    /// Was cancelled, timed out, or calculation threw an exception; no
longer running.
    case threw(error: Error)
}

/// An error that signals the future was cancelled.
enum CancelFuture: Error {
    /// 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).
    case cancelled
}

/// Base class for futures; acts like a future that was cancelled, i.e. no
result and threw `CancelFuture.cancelled`.
/// - note:
///   - You would normally program to `Future`, not one of its derived
classes, i.e. arguments, return types, properties, etc. typed as `Future`.
///   - 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.
///   - This class is useful in its own right; not just a base class, but
as a future that is known to be cancelled.
class Future<T> {
    /// The current state of execution of the future.
    /// - note:
    ///   - 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.
    ///   - 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.
    ///   - 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.
    ///   - Status however offers detailed information if a thread
terminates by throwing (including cancellation and time out) and is
therefore very useful for debugging.
    /// - note: In the case of this base class, always cancelled; returns
`.threw(error: CancelFuture.cancelled)`.
    var status: FutureStatus<T> {
        return .threw(error: CancelFuture.cancelled)
    }

    /// 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.
    /// 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.
    /// - note:
    ///   - Timeout is only checked when `get` is called.
    ///   - 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.
    /// - note: In the case of this base class, always return nil.
    var get: T? {
        return nil
    }

    /// Cancel the calculation of the future; if it has not already
completed.
    /// - note:
    ///   - Cancellation causes `CancelFuture.cancelled` to be thrown and
hence the future's status changes to `threw`.
    ///   - 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).
    ///   - If a future timeouts, it cancels its calculation.
    ///   - If the future's calculation respects its `isCancelled` argument
then a timeout will break a deadlock.
    ///   - 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.
    /// - note: In the case of this base class, cancel does nothing since
this future is always cancelled.
    func cancel() {}
}

/// A future that calculates its value on the given queue and has the given
timeout to bound wait time when `get` is called.
final class CalculatingFuture<T>: Future<T> {
    private var _status = FutureStatus<T>.running // Really like to mark
this volatile and atomic (it is written in background thread and read in
foreground)!

    override var status: FutureStatus<T> {
        return _status
    }

    private let group = DispatchGroup()

    private let timeoutTime: DispatchTime

    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)!

    /// - note: The default queue is the global queue with default quality
of service.
    /// - note:
    ///   Regarding the `timeout` argument:
    ///   - Timeout starts from when the future is created, not when `get`
is called.
    ///   - The time used for a timeout is processor time; i.e. it excludes
time when the computer is in sleep mode.
    ///   - The default timeout is 2 seconds.
    ///   - If the calculation times out then the calculation is cancelled.
    ///   - 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.
    ///   - Also see warning below.
    /// - warning:
    ///   Be **very** careful about setting long timeouts; if a deadlock
occurs it is diagnosed by a timeout occurring!
    ///   If the calculating method respects its `isCancelled` argument a
timeout will also break a deadlock.
    init(queue: DispatchQueue = DispatchQueue.global(), timeout:
DispatchTimeInterval = DispatchTimeInterval.seconds(2), calculation:
@escaping (_ isCancelled: () -> Bool) throws -> T) {
        self.timeoutTime = DispatchTime.now() + timeout
        super.init() // Have to complete initialization before result can
be calculated.
        queue.async { [weak self] in
            guard let strongSelf = self else { // Future is no longer
required; it no longer exists.
                return
            }
            strongSelf.group.enter()
            do {
                guard !strongSelf.isCancelled else { // Future was
cancelled before execution began.
                    throw CancelFuture.cancelled
                }
                let result = try calculation {
                    strongSelf.isCancelled
                }
                guard !strongSelf.isCancelled else { // Future was
cancelled during execution.
                    throw CancelFuture.cancelled
                }
                strongSelf._status = .completed(result: result)
            } catch {
                strongSelf._status = .threw(error: error)
            }
            strongSelf.group.leave()
        }
    }

    override var get: T? {
        guard !isCancelled else { // Catch waiting for a cancel to actually
happen.
            return nil
        }
        while true { // Loop until not running, so that after a successful
wait the result can be obtained.
            switch _status {
            case .running:
                switch group.wait(timeout: timeoutTime) { // Wait for
calculation completion.
                case .success:
                    break // Loop round and test status again to extract
result
                case .timedOut:
                    isCancelled = true
                    return nil
                }
            case .completed(let result):
                return result
            case .threw(_):
                return nil
            }
        }
    }

    override func cancel() {
        switch _status {
        case .running:
            isCancelled = true
        case .completed(_):
            return // Cannot cancel a completed future.
        case .threw(_):
            return // Cannot cancel a future that has timed out, been
cancelled, or thrown.
        }
    }
}

/// A future that doesn't need calculating, because the result is already
known.
final class KnownFuture<T>: Future<T> {
    private let result: T
    override var status: FutureStatus<T> {
        return .completed(result: result)
    }

    init(_ result: T) {
        self.result = result
    }

    override var get: T? {
        return result
    }
}


/// A future that doesn't need calculating, because it is known to fail.
final class FailedFuture<T>: Future<T> {
    private let _status: FutureStatus<T>

    override var status: FutureStatus<T> {
        return _status
    }

    init(_ error: Error) {
        _status = .threw(error: error)
    }
}


// Example from
https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619
// Various minor changes necessary to get example to compile, plus missing
types and functions added.

// Needed to make the example run.

struct Image {
    let dataPath: String
    let imagePath: String
}

struct Resource {
    let path: String
}

func loadWebResource(_ path: String, completion: (_ dataResource:
Resource?, _ error: Error?) -> Void) {
    completion(Resource(path: path), nil)
}

func decodeImage(_ dataResource: Resource, _ imageResource: Resource,
completion: (_ imageResult: Image?, _ error: Error?) -> Void) {
    completion(Image(dataPath: dataResource.path, imagePath:
imageResource.path), nil)
}


func dewarpAndCleanupImage(_ image: Image, completion: (_ imageResult:
Image?, _ error: Error?) -> Void) {
    completion(image, nil)
}

// The example in 'completion-handler' form (with typo fixes).
// Doesn't actually run on a background thread; for simplicity, unlike
`Future` form.

func processImageData2(completionBlock: @escaping (_ result: Image?, _
error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult, nil)
                }
            }
        }
    }
}

func errorPrint(_ error: Error?) {
    guard let error = error else {
        return
    }
    print(error)
}

func imagePrint(_ image: Image?) {
    guard let image = image else {
        return
    }
    print(image)
}


// Not actually running on a queue so don't need to wait for completion!
print("Completion-handler form")
processImageData2 { image, error in
    imagePrint(image)
    errorPrint(error)
}

// Compiler generated, `Future` versions of functions with continuation
handlers.

enum ResultOrError<T> {
    case result(T)
    case error(Error)
    case notInitializedYet
    func value() throws -> T {
        switch self {
        case .result(let result):
            return result
        case .error(let error):
            throw error
        case .notInitializedYet:
            fatalError("Bug in automatically generated function.")
        }
    }
}

func loadWebResource(_ path: String) -> Future<Resource> {
    return CalculatingFuture { _ in
        var resultOrError = ResultOrError<Resource>.notInitializedYet
        loadWebResource(path) { dataResource, error in
            if error == nil {
                resultOrError = .result(dataResource!)
            } else {
                resultOrError = .error(error!)
            }
        }
        return try resultOrError.value()
    }
}

func decodeImage(_ dataResource: Resource, _ imageResource: Resource) ->
Future<Image> {
    return CalculatingFuture { _ in
        var resultOrError = ResultOrError<Image>.notInitializedYet
        decodeImage(dataResource, imageResource) { image, error in
            if error == nil {
                resultOrError = .result(image!)
            } else {
                resultOrError = .error(error!)
            }
        }
        return try resultOrError.value()
    }
}

func dewarpAndCleanupImage(_ image: Image) -> Future<Image> {
    return CalculatingFuture { _ in
        var resultOrError = ResultOrError<Image>.notInitializedYet
        dewarpAndCleanupImage(image) { imageResult, error in
            if error == nil {
                resultOrError = .result(imageResult!)
            } else {
                resultOrError = .error(error!)
            }
        }
        return try resultOrError.value()
    }
}

// The example in 'future' form (runs on a background queue).
func processImageData1() -> Future<Image> {
    let dataResource  = loadWebResource("dataprofile.txt") // dataResource
and imageResource run in parallel.
    let imageResource = loadWebResource("imagedata.dat")
    let imageTmp      = decodeImage(dataResource.get ?? Resource(path:
"Default data resource or prompt user"), imageResource.get ??
Resource(path: "Default image resource or prompt user"))
    let imageResult   =  dewarpAndCleanupImage(imageTmp.get ??
Image(dataPath: "Default image or prompt user", imagePath: "Default image
or prompt user"))
    return imageResult
}

print("\nFuture form")
let processedImage = processImageData1()
let _ = processedImage.get // Wait for calculation to complete or timeout.
print(processedImage.status) // Status is useful for testing!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170824/87ee717f/attachment.html>


More information about the swift-evolution mailing list