Guidance for suspend functions


#1

I finally bit the bullet and wrote my first 100% kotlin app (something > 10 lines).
–> background
The use case is batch uploading to AWS DynamoDB using the Java Async SDK. (Im working with the 2.0 beta).
It’s a subtly challenging problem in that to get any decent performance you have to not only use a great deal of
parallel IO but also have precise rate/flow control over long periods or your API calls are throttled and eventually refused
generating exceptions, and if not handled perfectly, unexpected/unknown loss of data (not on the server side, due to client code if you don’t trap and resend requests on errors including some kind of exponential backoff ).
The 2.0 API Async code uses CompletableFuture<> for everything - which is a very verbose API at best - and if you havent used it before, a steep learning curve on its own.

So on top of learning which side of a “:” the type goes on, var vs val, toss in a Beta API with unknown bugs, NIO , multi threading, why not go all the way and try the ‘experimental’ coroutines. Maybe even figure out what ‘coroutines’ are.
Perfect use case for learning a new language :wink:
But hey, if the language (and I) can survive that - its worthing looking at.
----->
Skip to the end –
Its worth looking at. “Impressive” doesnt come close.

What surprised me was my first real problem was that it was TOO FAST.
Using ‘traditional’ Java, Python, or cli tools (based on python) – the best I could previously get was about 100/req/sec.
(batches of 25).
My first barely working ‘hello world’ quality kotlin app I had to kill it because it was hitting 10,000 req/sec out the gate blowing away the server side rate limits then crashing with exceptions caused by the http errors.
Its rare I get the opportunity to work on code that is too fast – that changed the design and implementation challenges.
This is the reason I wanted to try kotlin – my theory being if I could get the ‘mundane’ code out of the way faster then
for any given project Id have more time to work on the harder parts rather than having to stop after its ‘barely functional’.
In this case, 90% of the time allotted went where I did not predict, writing robust rate limiting code that worked well with both kotlin and the NIO/Completable future APIs. Turned out to be a great way to see where the ‘brick wall’ is – there isn’t one. So now that I’ve fully drunk the kool-aid and probably never going back to ‘plain java’ again – I need to figure out the difference between what I hacked together vs “The Right Way” – in particular wrt to coroutines
---->
Actual Question:

I’m still uncertain how to determine as a ‘rule of thumb’ when one should use “suspend” modifier on functions.
Its obvious when the compiler tells you (which is pretty amazing given most of the coroutine support is libraries not builtin) – But its not obvious when you ‘should’ or ‘shouldn’t’ – or how to tell if your wrong.
Reading up on some internals discussions – it seems that one is ‘not sure’ its better to use suspend then not.
I.e. if you are calling non-kotlin code or 3rd party deeply nested code that the compiler cant fully analyze, the side effects of making the wrong choice are worse if you omit suspend then if you added it but it wasn’t needed.
GIven a simplistic example -

suspend fun trickthecompiler() { 
    if( random() ) callSomeBlockingCode() else callSomeNonBlockingCode()
}

for that to work reliably, i am presuming that a function with ‘suspend’ modifier doesn’t actually have to call any blocking code, and nothing bad will happen. But given that ‘suspend’ isn’t implicitly added to ALL functions by the compiler – I am presuming that one shouldn’t just frivolously add it to all functions ‘just in case’ - that there is some downside.

So what is the downside ? What do you lose by adding ‘suspend’ ?
Is there runtime overhead? There is likely some involved to handle passing the implicit context around.
Is there compile time overhead ? There must be some - but enough to worry about ?
Is there a more subtle issue like behaviour changes running in a ‘suspend’ function that one needs to be aware of ?

Is there any better guidance for the cases where you don’t know if a function will block (or call some function that requires being in a coroutine but obscure enough the compiler didn’t catch it).
Does even adding ‘suspend’ help in those cases ?
I.e if it cannot be deduced at compile time, will adding ‘suspend’ actually help at runtime ?
Any suggestions for picking a strategy to determine what to do in the cases you are not 100% sure.

A) Put ‘suspend’ on every function 'just to be safe’
B) Only put ‘suspend’ on functions both you and the compiler agree are required.
C) Put ‘suspend’ on functions that may “block” – whether or not you know how or by what means.
D) Guess.
???)

And the corelary

What to look for as symptoms of having made the wrong choice.

A) Program crashes and computer explodes in a fireball
B) “Fishy” behaviour
D) Threads mysteriously lock up
E) Functions are being called on unexpected threads/stacks
F) Poor performance
G) Guess

-David Lee

.


#2

There’s a great video by @elizarov that may explain some things for you:

These two docs were also of great help to me:


Otherwise, afaik, the first answer is B (though the compiler won’t really care if you agree or not :), and the second answer depends on the details of what exactly you’re doing… B, D, E and F all seem possible, A luckily not so much :slight_smile:

If you put suspend on functions that don’t need it, nothing much happens - other than an extra hidden parameter (for the continuation), the function’s bytecode stays exactly the same, as far as I can tell.


#3

There is both runtime and compile-time overhead for suspend functions. You should not be adding suspend modifier everywhere, unless you need your function to suspend.

Nothing else. Otherwise, everything works as usual inside suspending functions.

The suspend modifier only helps if you have suspending invocations in the body of your function. Without them (if all you have is regular/blocking code) it is absolutely useless and brings no benefits (just overhead).

B here.

If you just put suspend everywhere you’ll just get worse performance. Also, you will make your code harder to reason about as it will not be clear from just looking at the function signature on whether this function can work for a long time (and suspend waiting for something) or not. This is useful thing to know about your function. Take a look at Math.sin and uploadMyFile for example. They are conceptually two very different operations – the first one takes short and predictable time to complete, while other might depend on external factors like network availability, etc. It is useful to know that just from the fact that the second one has suspend modifier.