Async function calls work like "normal" until they hit a yield statement. When they do, they yell back up the callstack "hey, I'm going to be waiting for a while, let something else happen while I am".
When that happens, one of two things can happen. 1. Either there's nothing else to do, in which case we wait until something that has yielded to no longer be waiting. 2. Something else is done waiting and it's waiting for its turn. In which case, it's allowed to continue at this point until it either returns or yields again.
In this sense, instead of yielding a value, the function is yielding the stack to something else that can use it while it's waiting.
Async is great for when you're waiting on external processes to complete (database calls, API calls, etc). It's terrible for any CPU heavy tasks because, at least in the Python world, it can still only use a single CPU. In this way, again, specific to Python, it's use-case is much closer to multithreading than multiprocessing.
Another way to think about the machinery behind the scenes is it's a queue of callstacks. If an async call yields the primary stack, its callstack is thrown onto the queue. The queue of callstacks is then processed in order.
Hope that helps! Took me a long while to wrap my head around it and I still struggle reading it some of the time.