Oh but you do, until you stop the flow with a do - catch
. I asked that same question yesterday, how to stop the async chain, and the answer was with beginAsync
, so it seems very similar. HAven't had time to read about exactly how it's used yet.
The way I understand it (which may be completely wrong, since I’m not at all a specialist of this topic), the bottom is at the point where you don’t care about sequentiality: the point where you call an async
function but do not have any code that requires execution of that function to complete before if can run.
If function A
is marked async
, it means that at some point during its execution, it will get suspended, then resume later after some kind of event, and then finish its job.
When function B
calls A
using await
, it means that it needs to wait until the execution of A
is complete before whatever code comes next can run (even in the case when what comes next is just the end of B
’s body). Which means that it accepts to see its execution suspended when A
’s execution gets suspended — hence making B
an async
function too.
However, if function C
needs to call B
but, after that, does things which do not depend on either the result returned by B
nor the effects B
may produce, then there is no reason for C
to wait for the completion of B
. It can call B
in a beginAsync
block, meaning that as soon as the execution of B
gets suspended, the thread will continue executing the rest of C
(after the block). Hence C
will not have to be marked async
.
At least, that’s what I’ve understood beginAsync
to be for.
Syntax-wise, I find it a bit cumbersome and I would prefer to be able to just call an async
function with no await
when I don’t need to wait for its completion (with a let _ = …
if it’s a non-Void
function, to explicitely state that I don’t care about its result).
But I’m sure some people will say that such a lack of an explicit marker would make it too easy to write bugs.
Of course, as I said at the beginning of this post, I may just have misunderstood the whole thing.
Sorry to dwell on this, but given that we need to put a beginAsync
at the bottom (or top depending on your point of view I guess), it looks like the result will be very similar to what you'd have today if you made the calls blocking but on a background queue:
// Old way
func processImageData2() -> Image {
let dataResource = loadWebResource("dataprofile.txt")
let imageResource = loadWebResource("imagedata.dat")
let imageTmp = decodeImage(dataResource, imageResource)
let imageResult = dewarpAndCleanupImage(imageTmp)
return imageResult
}
func usage2() {
someQueue.async {
let result = processImageData2()
// Do something with result on the main thread perhaps
}
}
// New way;
func processImageData1() async -> Image {
let dataResource = await loadWebResource("dataprofile.txt")
let imageResource = await loadWebResource("imagedata.dat")
let imageTmp = await decodeImage(dataResource, imageResource)
let imageResult = await dewarpAndCleanupImage(imageTmp)
return imageResult
}
func usage1() {
beginAsync {
let result = await processImageData1()
// What can I actually do with result, can I bring it outside this context?
}
}
Granted I am not sure I've understood it completely, but is there really any functional difference between these? Visually at least they are almost identical, so what is actually the big win?
I also realise that I haven't understood how, in the first case, I would use result outside of the beginAsync context. Can I assign it to a property? Use it on the main thread? I won't be able to return it, but that's fine, if I had wanted that I'd make usage1
async also.
It seems kind of problematic to me that, while the approach to async/await proposed at the top of this thread differs in certain significant ways from the previous proposal, most of the discussion has instead been about async/await in general and why we would want it in any form, and the line between general concept and specific proposal has gotten blurred.
As I understand it (and please correct me), @QuinceyMorris is proposing an approach where the transition between sync and async code is pretty transparent, while the Lattner/Groff proposal uses explicit calls like beginAsync
to manage that transition. The catch is that the transparent version hasn't been fully hammered out, and requires further discussion to determine how (or even whether) it could be accomplished. That discussion, of course, needs to happen among people who already understand the async/await concept, and in this thread that discussion has gotten lost among questions about the basics of the idea instead.
I'm tempted to say we need two threads - one general and conceptual, and one about actual implementation. Or at least, we need to be more explicit about what things are common to all async/await approaches, and what things (like sync/async transition management) are specific to certain languages or proposals.
I think this has actually already been answered upthread, with the conclusion that what was proposed is not possible.
This part of the proposal in the OP is that await
be usable from synchronous functions, where it would make the function wait until the completion of the execution of the async
function, but with the following constraint:
Waiting (as
await
does inA1
) must not block the thread it’s running on. It only blocks its own path of execution.
The problem I see is that this constraint defines an asynchronous function.
So, this part of the proposal does not actually describe a way to use await
in a synchronous function.
In processImageData2
, you are calling synchronous versions of loadWebResource
, decodeImage
, and dewarpAndCleanupImage
, which would block the (background) thread until they complete their execution.
In order to make processImageData2
really equivalent to processImageData1
, you need to rewrite it using a callback-based version of (at least) loadWebResource
. The resulting code will make the benefit of async/await
more obvious.
In this case, the real benefit of async/await
is not for functions like decodeImage
and dewarpAndCleanupImage
, which, presumably, just run potentially long computations and will block a thread anyway while doing so (we just don’t want it to be the UI thread). It is for functions like loadWebResource
, which we don’t want to block a thread while just waiting for data from the network.
Yes, you just need to include the code for that in the beginAsync
block, after the call to processImageData1
(which needs to use await
, btw).
Thank you for the reply!
I mean, what does it even mean to block a background thread? Sure for that thread it means something, but for the app and the CPU it seems like the effect would be the same as async/await? There you also block the context you're in, both for decodeImage
and the network calls, right?
The methods in processImageData2
were sync on purpose btw, that's part of my point, that sync and sequential on a background thread seems to be equivalent to async/await?
I think my question can be boiled down to this:
Why do you want to avoid blocking a background thread while you're waiting for some web resources or something. The CPU is not tied up, so the other threads don't suffer.
This is a key question. The programming model of threads works as you suggest; you write synchronous code that can block freely, and the scheduler fills in the bubbles with other work. But the reality of kernel-space implementations of threading is different; threads and context switches are too expensive to do this in a fine-grained or large-scale way.
This leads to broadly two kinds of solutions (or workarounds): lightweight user-space (“green”) threads, or tools to make asynchronous code on top of thread pools easier and safer to write. Async/await and Dispatch fall in the latter category.
Ok, I'll jump in here. Let's ignore iOS here, but think about a sever-side Swift application. Threads are not free - they have a cost both in memory terms and jumping between them and switching out the stack. So for high IO application, or high performance applications (like those built with SwiftNIO) you typically have a small pool of threads, something like the number of virtual cores on your CPU. So you can't just block the thread because you won't be able to handle any more requests on that thread.
NIO has a very good model for dealing with this in an asynchronous way, with its EventLoopFuture
. But writing readable code with those without a concurrency model is hard. For loops have to become recursive functions and it's very easy to end up in a pyramid of doom resolving multiple futures.
Thanks for the replies, now I got it. So it's a sort of a second order effect that has to do with performance, not a behavioural difference. To be honest I had exactly the same problem when trying to understand the point of the EventLoopFuture (as opposed to normal Futures which made a lot of sense) in NIO so that's two birds with one stone.
Another reason why you would want to not block the current background thread, is to nonatomically modify some global variable
var cachedAnswers: [Int:Int] = [:]
async func precomputeAnswer(for x: Int) {
let answer = await answer(for: x)
cachedAnswers[x] = answer
}
You can run 100 instances of that on a single background thread to quickly precompute the answers and put them in a dictionary. If await
blocked the background thread, it would be very slow, and if you ran it on different threads it would be fast, but violating the thread safety.
Looking back at the proposal, I found the suggested DispatchQueue.syncCoroutine()
function interesting, where you can switch to the given queue for the rest of your async
function.
A logical step from there is to be able to start an async
context on a specific queue (or thread), and I realized you could accomplish it with the above function, something like:
extension DispatchQueue
{
func beginAsync(_ body: () async throws -> Void) rethrows
{
try Swift.beginAsync { // or whatever module the main beginAsync() ends up in
self.syncCoroutine()
try body()
}
}
}
...but that runs into the overloading of "async" where we have the DispatchQueue
sense of "run this some time in the future on this queue" and the async/await sense of coroutines and such. This could be an argument for using alternative terminology such as yields
.