You can always create multiple pools:
ctx = multiprocessing.get_context("fork")
with (
#Parenthesized context managers new in 3.10 !!!
ctx.Pool(20) as p1,
ctx.Pool(10) as p2
):
r1 = p1.imap_async(task1, args1)
r2 = p2.imap_async(task2, args2)
for a, b in zip(r1, r2):
print(a, b)
Idle workers won't consume much CPU, so it's not a bad idea to slightly over-provision each sub-pool. They do consume some memory however, so keep that in mind.
Edit Regarding running more processes than cores:
Your computer has hundreds of processes going at any time (on most modern operating systems) it is the job of the operating system scheduler to prioritize each one based on how active they are. If you have more processes than you have physical cores, the OS will be actively (and rapidly) swapping between them to make sure each one runs at least once in a while (probably on the order of miliseconds). The process of switching is fast but not completely free, so it does hurt performance to create many many more processes than you have cores.
If you have enough work for all 30 workers to remain busy, and they are all assigned the same priority by the OS (they will unless you change it), they will roughly split the cpu time 2:1 as each process should get roughly equal time. If you run out of task2
to work on, the cpu will stay fully busy with 20 workers, though if you run out of task1
, you'll only use 50% of your cores. I've used strategies like this in the past, and it's simple and functional. In reality, if you need to squeeze every last drop of performance from your cpu, you'll need to do a lot of testing and heuristic based tuning for a method like this to work(which will need to be constantly updated any time any code or data changes).
If you do need something more complicated, you could create your own "Pool
" from Process
, Queue
, Lock
, etc.. with the exact number of workers as you have cores, and manually set priority (eg; pulling tasks for the children from a priority queue).