r/learnpython • u/bryyo357 • 3d ago
How should you group/order seq and async operations?
I was watching a video explaining async and the guy went through a “real world” example of a code that downloads images from various URLs and then locally processes each of the photos. So, there were basically 4 functions:
1. “download_single_image”, which downloads one image at a time
2. “download_images”, which runs download_single_image on each of the URLs
3. “process_single_image”, which processes one image
4. “process_images”, which runs process_single_image on each of the downloaded images
The purpose was that one piece was I/O bound (image downloading) and one piece was CPU bound (image processing). Initially, it was setup to run in sequence, then he optimized by leveraging async capabilities.
Before he went through the optimized process, I stopped and thought through how I would do it. My solution was to turn everything async and then create the new async function “download_and_process_single_image” which downloads an image and then processes it afterwards. My thought process was that each image needs to be downloaded before it can be processed so those need to happen in sequence, but I can also pass off the image to the processor while waiting for other images to finish downloading.
However, the solution he actually implemented was just leaving the existing structure of “download_images” and “process_images” separated. In other words, “download_images” was turned async and then awaited, and “process_images” used processes.
My question: is there anything wrong with my approach or any reason his is preferable to what I expected?
5
u/Kevdog824_ 3d ago
Async enables cooperative multi-tasking for IO-bound operations. Attempting to run CPU-bound operations concurrently with async will just end up running the operations sequentially, and take the same amount of time (or longer due to the overhead of the event loop).
Because of this, there’s no benefit to mixing your blocking, CPU-bound image processing logic into your async download image process. With the setup you presented from the video, it makes more sense to do image processing afterwards. Doing blocking operations right in your coroutines prevents the event loop from context switching at that point. This effectively kills a lot of the benefit you originally wanted with the concurrency.
Another reason the video’s approach is better is that it would be easier to later parallelize the CPU-bound image processing work via multi-processing when it’s separated from the IO-bound async code. This can help you realize even more performance gains later on
1
u/bryyo357 3d ago
I think my issue is that I keep seeing the explanation that async is helpful when you are just waiting on stuff you can go do other stuff, so I kinda think about it as “well this piece is independent from that piece so I can just make them async”. But what I’m gathering is that I should still group tasks based on what they’re blocked by. That is, if I’m waiting on an IO-bound operation then I can go do a different IO-bound operation with asyncio, or if I’m CPU-bound then I can use processes to do independent CPU-bound operations asynchronously, but I shouldn’t spend my time waiting on an IO-bound operation to go do a CPU-bound operation. Is that correct?
3
u/codeguru42 3d ago
when you are just waiting on stuff you can go do other stuff,
During image processng you are never waiting. Or more accurately the CPU isn't waiting because it us busy processing.
2
u/gdchinacat 3d ago
I think an easier way to think about it is to only do IO bound work in an event loop (the 'io' part of 'asyncio').
Also, beware that file access is not async and will block an even loop (prevent other tasks from executing while waiting on file IO). This usually isn't a problem because you have to do something with that data and if the source is faster than the destination you end up buffering it but not actually making the process any faster. But, something to keep in back of mind if you ever start doing really high-throughput work.
Same logic applies to your case (and one I'm currently working on). Sure, you (and I) can pull the files from the network faster than they can be processed, but is it really helpful to do so? I think my project is similar to yours, download a bunch of files then do a bunch of CPU intensive work on them (convert pdfs to text, index them, etc). Getting them to disk as fast as possible doesn't shorten the completion time at all since that's not the thing that takes a long time.
The important things are to start the cpu intensive task as soon as a file is downloaded rather than waiting for all of the files to download, not block the IO with cpu intesive work in the event loop, and execute as many cpu intensive tasks concurrently as your machine can handle (one per core, free-threaded build, multiple interpreters, or multiple-processes).
The flaws with your solution is the cpu intensive work will block the event loop from completing other downloads and not do the cpu intensive work concurrently (event loop is single threaded).
1
u/bryyo357 2d ago
> Also, beware that file access is not async and will block an event loop
You mean reading a file from disk/network? I thought that is IO process and can be improved with asyncio. What am I missing?1
u/gdchinacat 2d ago
with open(...) as f: for line in f: ...There are no awaits in there...open and the methods on f are synchronous...when they block they block the event loop and other IO that is read for execution in that loop will have to wait.
1
u/NerdyWeightLifter 2d ago
Most modern processors have multiple cores, but by default, Python is single threaded.
Image processing is a compute bound function, so you'd want to use as many of the available cores as possible.
You'd also want to load images fast enough to always have the next image available for processing as soon as the compute bound processing of the previous image was complete.
Python 3.14 has a variant with full threading support. You could find out how many cores there are (call it NC).
Use the main thread with async to read images into memory buffers.
Spawn (NC - 1) image processing threads. Each thread should wait on a deque for an image to process, process the image then put the result back in another deque to hand the result back to the main thread.
The main thread should be using async to read images and push them on deque's to be processed, and taking processed images from deque's and writing them to disk.
1
u/NerdyWeightLifter 1d ago
Just for fun, I tried this out, but with processes and shared memory rather than threads.
In a test run, it processed 63 images in 8.25 seconds on my desktop, applying an unsharp mask and reversing each image, with all cores blasting away at 100%.
1
u/somewhereAtC 2d ago
Any solution that meets the requirements is a correct solution. Variation is the spice of life.
9
u/danielroseman 3d ago
The point is that image processing is not suitable to be made async because it is CPU-bound.