Hi. It’s not clear what you are trying to do. Do you want to measure the time it takes to switch to a different coroutine? If so, why do you want to do that?
This code benchmarks the Python event loop. It looks fine, but there’s no need to iterate up to 10000000. Looping 10000 times is sufficient. Create a variable to store the number of iterations.
Okay, but you won’t be testing that by counting any sort of switching interval. If you want to test something handling thousands of requests a second, youl’l need some sort of actual request to handle.
When all your coroutines are doing is sleeping for zero seconds, you’re basically just spinning a CPU core, so it’s basically testing nothing. Try having an echo client and server running in a spam loop, or having a multiplexed server that actually gets thrown thousands of socket connections per second. What you’ll most likely find is that (a) a thread per connection will quickly run you out of resources; (b) a pure coroutine-based solution will cap out at one CPU core; and (c) a thread pool works really well but is hard to measure. Actually you’ll probably find “is hard to measure” for all of them. Have fun though
You cannot beat coroutines on a single core. Threads are heavyweight compared to coroutines; that is, most of the resources are spent on context switching in real-world scenarios. For example, try using a network simulator that adds deliberate latency.
A rule of thumb is as follows: use coroutines for I/O-bound algorithms, threads for memory-bound algorithms, and multiprocessing for CPU-bound algorithms.
Efficient doesn’t really mean lower response times in absolute terms.
If you’re only ever handling 1 request at a time, a sync approach will probably get you slightly lower latency simply due to the fact there’s less actual Python code (and code/instructions in general) to run on the entire code path. There’s no event loop. But production servers don’t handle 1 request at a time so other variables enter the picture.
The async approach also allows a more efficient style of application architecture and has a better cancellation model, which are also important considerations.
Or alternatively: The async approach gives a useful abstraction (at an acceptably-small cost) over a more traditional select-based [1] architecture. An efficient web server might be designed something like this:
Create listening socket, bound to well-known port (possibly multiple eg if you listen on both 80 and 443)
Wait for any of these sockets to be readable, any of these sockets to be writable.
If the listening socket is readable, accept a new socket. It is now part of the collection of “waiting to be readable”.
If a connected socket is readable, read from it. Process that. Do we have a full request? Handle the request. Write to the socket. Too much to write? Add this socket to “waiting to be writable”.
If a connected socket is writable, write to it.
Rinse and repeat.
This is efficient, but a bit of a pain to work with. But this architecture can be split into an event loop and a set of tasks, so it ends up like this:
Main task:
Create a listening socket.
Wait for this socket to be readable.
Accept socket. Spawn task to handle it.
Repeat.
Socket task:
Read from socket. Do we have a full request? If not, keep reading.
Process request.
Write the response. Couldn’t write it all? Wait for it to be writable, then write more.
The event loop, meanwhile, just has a list of tasks and what they’re waiting on. You’re waiting for this socket to be readable, you’re waiting for that socket to be writable, maybe that one’s waiting for a subprocess to finish, whatever it be. Once it gets a signal from select(), all it has to do is run the corresponding task.
Thought of this way, async I/O isn’t really like threads, but it’s able to look like threads to the programmer.
You wouldn’t directly call select() in asyncio-based code. But in something that’s built around that (like the example I gave of how you’d do it manually), select() is a blocking call that tells you when “something” is ready - usually a file (eg a socket) being ready, or a timeout. So it’s the blocking call that allows everything else to be nonblocking without the need to spin the CPU.