Skip to content

Instantly share code, notes, and snippets.

@davbeck
Created August 18, 2017 18:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davbeck/e3b156d89b2e9d97bb5a61c59f8a07f7 to your computer and use it in GitHub Desktop.
Save davbeck/e3b156d89b2e9d97bb5a61c59f8a07f7 to your computer and use it in GitHub Desktop.
Swift Async / Await thoughts

Async Await

I love this proposal so much. Much of it is exactly how I’ve thought Swift’s concurrency model should look over the last year.

Making async a return attribute just like throws seems like the right solution to me. Building on top of callbacks (rather than introducing futures/promises) is also the right approach for swift. I think this proposal nails the problem right on the head: callbacks don't work well with the rest of Swift's error handling, is awkward, error prone, and yes, looks ugly.

One point that I've gone back and forth on is how strictly to enforce excecution order. For instance, in this example. it would make sense to allow the first 2 lines to excecute in parallel and excecute the third line once they both complete:

let a = await foo()
let b = await bar()
return [a, b]

But not in this case:

await client.connect()
let rooms = await client.getRooms()

In the first case the compiler could automatically optimize to run in parallel, the second, it could not. I like the idea of wrapping parallel code in a future, making the operation explicit and clear.

Excecution context

I’m not familiar with C# or other implementations of async/await, only with Javascript’s (very new) implementation. I’m curious how C# handles execution contexts (threads, queue etc) since JS doesn’t have to deal with that.

The syncCoroutine and asyncCoroutine example seems weird to me. It's also unclear in that example what the context would be after a call to async. Would excecution return to the queue, or be on whatever queue the async function called back on? It makes a lot more sense to me to represent this with code blocks, something like:

doSomeStuff()
await startAsync(mainQueue) {
	doSomeStuffOnMainThread()
}
await startAsync(backgroundQueue) {
	doSomeStuffInBackground()
}

Where every line in the code block is run on the context. This doesn't handle synchronous excecution though. For instance, if we wanted to block a queue until the entire async function had returned. An alternative might be to have queues and other contexts define their own method that took async functions:

doSomeStuff()
mainQueue.sync {
	await loadSomethingStartingOnMain()
	doSomeStuffOnMainThread()
	// don't let anything else exceute on the main queue until this line
}
await mainQueue.async {
	doSomeStuffInBackground()
}

Using queue.async taking an async function might be a good alternative to a language "startAsync". Having the excecution context undefined based on whatever queue the underlying code calls back on seems dangerous to me. Forcing the user to define the context would fix that, but at the cost of introducing extra dispatch calls where they may not be needed. A general purpose context, that simply continued excecution in place would fix that, and be more explicit when it was needed.

Conversion of imported Objective-C APIs

One issue I see with the importer is that the conventions for callbacks aren’t as strict as NSError ** methods were. For instance, URLSession completion blocks include response, data and error, all of which are optionals. The response is almost always present, even if there was an error. But there is no way to know that from the ObjC type system, and no way to represent a throwing function that also returns metadata on error in Swift.

There are also cases where you wouldn’t want ObjC callbacks to be imported as async functions. For instance, it wouldn’t make sense for NotificationCenter callbacks to be awaited. In general, any callback that can be called more than once is dangerous to use as an async function.

Personally, I would be in favor of taking a reserved but default on approach to importing ObjC functions as async, and adding annotations to ObjC to control their Swift importing. For instance, by default callbacks with either 0 or 1 arguments would be imported as async non-throwing and callbacks with 0 or 1 arguments plus an error would be imported as throwing async. Callbacks with more than 1 argument would need to be manually annotated. Methods that should not be async (like NotificationCenter) can be annotated to not be imported as async.

Another issue we’ll need to contend with is intermediate tasks. Both URLSession and the Photos framework come to mind. In the existing model, they return something that allows you to cancel the request while it is in progress. Consider the following example:

class ImageView {
	private var currentTask: URLSessionTask?
	
	var source: URL? {
		didSet {
			currentTask?.cancel()
			image = nil
			guard let source = self.source else { return }
			
			load(source: source) { image, error in
				guard self.source == source else { return }
				if let image = image {
					self.image = image
				} else {
					self.image = errorImage
				}
			}
		}
	}
	
	var image: Image?
	
	func load(source: URL, completion: @escaping (Image?, Error?) -> Void) {
		let task = URLSession.shared.dataTask(with: source) { (data, response, error) in
			guard let data = data else {
				completion(nil, error)
				return
			}
			
			let image = UIImage(data: data)
			
			completion(image, nil)
		}
		self.currentTask = task
		
		task.resume()
	}
}

How should we represent dataTask(with:completion:)? If I were writing this API from scratch, task.resume() would be the async function, but that may not be feasible for the importer.

If I could rewrite that example in a magical future version of Swift and Foundation, it would look something like this:

class ImageView {
	private var currentTask: URLSessionTask?
	
	var source: URL? {
		didSet {
			currentTask?.cancel()
			image = nil
			guard let source = self.source else { return }
			
			startAsync {
				do {
					let image = await try load(source: source)
					guard self.source == source else { return }
					self.image = image
				} catch {
					guard self.source == source else { return } // kind of awkward to have to write this twice
					self.image = errorImage
				}
			}
		}
	}
	
	var image: Image?
	
	func load(source: URL) async throws -> Image {
		let task = URLSession.shared.dataTask(with: source)
		self.currentTask = task
		
		let data = await try task.resume()
		
		return await UIImage(data: data)
	}
}

Actors

I fail to see the benefit of adding an entirely new construct for the actor model. It seems like access control, value semantics, and dispatch queues largely fill this need already, and the implimentation of an actor wouldn't be so complicated currently that it needs a compiler feature to ensure a correct implimentation.

Futher, it seems like giving up strict actor models would remove some of the benefits other languages have with their actor models. It has been quit a while since I worked with Erlang, but from what I remember, the magic of it's recovery model comes from it's recursive, functional model. It can recover from otherwise fatal errors because it can just reset it's state to the state before the last message was received. For reasons outlined already, Swift can't enforce that strictly, so no matter what we are going to have to rely on implimentations to be implimented correctly.

Perhaps I'm missing something though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment