If it can also receive data when it is resumed, you have an ES6 generator (or the C macros above!).
If its subroutines can cause it to suspend, you have a full-blown coroutine.
If it can be copied, you can build call/cc.
Full-blown coroutines give us a way to write fully asynchronous code without using callbacks. For example, I could define ASYNC macro as follows:
#define ASYNC(blocking_function_call) \ do { \ auto io_pool = GetIOThreadPool(); \ auto cpu_pool = GetCurrentThreadPool(); \ \ Detach_The_Thread_and_Enqueue_Coroutine_For_IO(io_pool); \ \ blocking_function_call; \ \ Detach_The_Thread_and_Enqueue_Coroutine_For_CPU(cpu_pool); \ } while (0)
Now, I can make any blocking operation asyncrhonous. For example, open() or readdir() functions do not have asynchronous interfaces, but I can make them asynchronous by simply wrap it with ASYNC(...) macro.
It works as follows: When we need to do a blocking operation, we suspend the coroutine and we enqueue it into a thread-pool that is designated to do blocking calls; the cpu thread is now free, so it can start executing other cpu-bound coroutines. When io thread has finished the job, it will enqueue the coroutine back to the CPU thread pool.
What we have now is a fully asynchronous system where cpu threads can be kept busy with only cpu-bound work and io threads can be kept with io-bound work. This gives us full control over the concurrency. We could be confident that CPU threads are always available for compute work.
We cannot do this with python "yield".
Python's PEP 342, which expanded python generators into truer coroutines, has a decent discussion on the differences.