Multi-Processing
This lesson includes the following topics:
The multiprocessing module was written specifically to offer features closely parallel to the threading library but allowing the individual threads of control to be processes rather than threads within a single process. This allows the operating system to take advantage of any parallelism inherent in the hardware design, since generally processes can run completely independently of one another, and on separate processors if they are available.
The multiprocessing library defines various classes, most of which operate in the same way as similar classes in the threading and related modules. Whereas in using threading you also imported resources from other modules, the multiprocessing module tries to put all necessary resources into one convenient place, simplifying imports. But you will easily recognize the program style from your recent work on multi-threading.
Our first multiprocessing example is marked up below as though we were editing the first thread.py example. This shows how similar the two environments are. Create process.py as shown:
"""
process.py: demonstrate creation and parallel execution of processes.
"""
import multiprocessing
import time
import sys
def run(i, name):
"""Sleep for a given number of seconds, report and terminate."""
time.sleep(i)
print(name, "finished after", i, "seconds")
sys.stdout.flush()
if __name__ == "__main__":
for i in range(6):
t = multiprocessing.Process(target=run, args=(i, "P"+str(i)))
t.start()
print("Processes started")
Note that this program has been correctly written as a module, so that the action of starting six processes is only performed by the process that runs this code, and not in any processes that may try to import the module. This is very important, because the subprocesses have to get their description of the work to be done from somewhere, and they do that by importing the main module. So in this case the subprocesses will import the process module (so the test __name__ == "__main__" is false) to access the run() function.
| Note | Not all platforms require that the main module be "importable" in that way. Since it does not hurt to write your programs this way, however, we recommend that you do so every time. Then, platform differences are less likely to "bite" you. |
| Modern Python | Since Python 3.8, the default process-start method on macOS has changed
from "fork" to "spawn". Under "spawn", the worker
interpreter imports the main module from scratch rather than inheriting a forked copy of
the parent's memory. This makes the if __name__ == "__main__": guard
essential on macOS and Windows — without it, every spawned worker would attempt to start
six more processes, causing an explosion of subprocesses. The example above is already
correctly written. Linux still defaults to "fork", so code that relies on
inherited state may work there but silently break on macOS; for portable code, always
use "spawn" semantics and ensure all worker-callable code is importable
without side effects.For higher-level parallelism, consider multiprocessing.Pool (which manages
a fixed pool of worker processes and exposes map(), apply(),
and friends) or concurrent.futures.ProcessPoolExecutor (a cleaner, more
modern interface compatible with the same Executor API used by
ThreadPoolExecutor).
|
The output should not be at all surprising:
Processes started P0 finished after 0 seconds P1 finished after 1 seconds P2 finished after 2 seconds P3 finished after 3 seconds P4 finished after 4 seconds P5 finished after 5 seconds
The lesson on multi-threading concluded with an example that used a pool of worker threads to convert the characters of a string into upper case. To demonstrate the (at least superficial) similarities between multiprocessing and threading and friends, we'll now adapt that code.
So first, copy the three programs (output.py, worker.py, and control.py from your previous lesson's folder to your current folder.
The following listing shows the code for the multiprocessor version alongside the equivalent threading-based code. The differences are small enough to be negligible, and to allow anyone who understood the threaded code to also understand the multi-process version.
"""
output.py: The output process for the miniature framework.
"""
identity = lambda x: x
import multiprocessing
import sys
class OutThread(multiprocessing.Process):
def __init__(self, N, q, sorting=True, *args, **kw):
"""Initialize process and save queue reference."""
multiprocessing.Process.__init__(self, *args, **kw)
self.queue = q
self.workers = N
self.sorting = sorting
self.output = []
def run(self):
"""Extract items and print until all done."""
while self.workers:
p = self.queue.get()
if p is None:
self.workers -= 1
else:
# This is a real output packet
self.output.append(p)
print("".join(c for (i, c) in (sorted if self.sorting else identity)(self.output)))
print ("Output process terminating")
sys.stdout.flush()
The main difference between the two pieces of code is the use of multiprocessing.process in place of threading.Thread, and associated changes to a couple of comments. It is also necessary to flush the process's standard output stream to make sure that it is captured before the process terminates—otherwise you will see a confusing lack of output! (Feel free to try running the program with the flush() call commented out to verify this).
The next listing shows the differences in the worker code when processes are being used instead of threads.
"""
worker.py: a sample worker process that receives input
through one queue and routes output through another.
"""
from multiprocessing import Process
import sys
class WorkerThread(Process):
def __init__(self, iq, oq, *args, **kw):
"""Initialize process and save Queue references."""
Process.__init__(self, *args, **kw)
self.iq, self.oq = iq, oq
def run(self):
while True:
work = self.iq.get()
if work is None:
self.oq.put(None)
print("Worker", self.name, "done")
self.iq.task_done()
break
i, c = work
result = (i, self.process(c)) # this is the "work"
self.oq.put(result)
self.iq.task_done()
sys.stdout.flush()
def process(self, s):
"""This defines how the string is processed to produce a result."""
return s.upper()
Again the only change is to use Process from multiprocessing instead of Thread from threading. (Two of the differences are again in comments.)
The control process again needs very little change: queue objects come from the multiprocessing module rather than the queue module, and in that module if you are going to join() a queue then you must use a JoinableQueue. The rest of the logic is exactly the same, with the exception that the code must now be guarded so that it isn't executed when the module is imported by the multiprocessing module. This means you have to indent the majority of the logic. This is easy in Eclipse: just highlight all the lines of code (making sure you are selecting whole lines) and then press Tab once.
"""
control.py: Creates queues, starts output and worker processes,
and pushes inputs into the input queue.
"""
from multiprocessing import Queue, JoinableQueue
from output import OutThread
from worker import WorkerThread
if __name__ == '__main__':
WORKERS = 10
inq = JoinableQueue(maxsize=int(WORKERS*1.5))
outq = Queue(maxsize=int(WORKERS*1.5))
ot = OutThread(WORKERS, outq, sorting=True)
ot.start()
for i in range(WORKERS):
w = WorkerThread(inq, outq)
w.start()
instring = input("Words of wisdom: ")
# feed the process pool with work units
for work in enumerate(instring):
inq.put(work)
# terminate the process pool
for i in range(WORKERS):
inq.put(None)
inq.join()
print("Control process terminating")
Running this version of control.py should do exactly what the threading version did, except that the individual characters are now being passed to one of a pool of processes rather than one of a pool of threads. The computation is trivial, but the principle would be the same if the work packets were filenames and the outputs were MD5 checksums of the contents of the file (which could require substantial computation and I/O in the case of long files). Since the processes run independently of each other, they can be run on different processors at the same time, allowing programs to take true advantage of hardware parallelism. The output will seem prosaic for the amount of work that is being done!
Words of wisdom: No words of wisdom at all, in fact. Just a rather long and boring line of text. Worker Thread-2 done Worker Thread-3 done Worker Thread-4 done Worker Thread-5 done Worker Thread-6 done Worker Thread-7 done Worker Thread-8 done Worker Thread-9 done Worker Thread-10 done Worker Thread-11 done Control thread terminating NO WORDS OF WISDOM AT ALL, IN FACT. JUST A RATHER LONG AND BORING LINE OF TEXT. Output thread terminating.
| Note | The output above shows Worker Thread-2 through Thread-11
and "Control thread terminating" / "Output thread terminating." because this sample run was
captured from the threading version; the multiprocessing version will display
process names such as Worker Process-2 and the messages will say "process"
rather than "thread". Process output ordering is non-deterministic and PIDs vary between
runs — the content shown is original source output kept verbatim. |
Do not make the mistake of thinking that this brief treatment has taught you all you need to know about multiprocessing. There are many more things to learn about it including, for example, limitations on what can be transmitted from process to process through a multiprocessing.Queue. These restrictions are fairly commonsense, and are the result of having to pickle the objects to transmit them to the remote process. As long as you stick to Python's basic data objects (and combinations thereof), you should be fine. Other restrictions are less obvious: when you subclass multiprocessing.Process, the instances should be pickleable (because the class has to be instantiated in a new process when the instance's start() method is called).
As systems evolve, multiprocessor solutions will become more and more common, and it will be necessary to put systems together to take control of multi-processor machines. This lesson is intended to give you the necessary grounding so that you can take the next steps with confidence.
