How to split a job between processes
To use several processors, you split one job into chunks, give each chunk to a separate worker and merge the answers. Splitting a job well is the skill behind every parallel program, from a laptop to a GPU.
A shop wants the total of a day's 40,000 orders. Four workers can each add a quarter of them, then one more step adds the four answers.
By the end of this lesson
Note
I can split a job into chunks, run them in separate processes and merge the answers.
Key words
| Word | What it means |
|---|---|
| Process | a running program with memory of its own |
| Thread | a worker inside a process that shares its memory |
| Chunk | one share of the data given to one worker |
| Partial result | the answer for one chunk alone |
| Merge | combine partial results into the final answer |
| Pool | a set of workers that take chunks in turn |
The cells students fill in are left blank in this preview.
A job is split into chunks and merged
Chunks are shares of the data, cut so that no item is in two chunks and none is left out. Each worker gives a partial result for its own chunk.
A final step merges the partial results. For a sum, merging is adding. Cutting the data and merging the answers cost time, and both are serial.
Worked example: 12 totals in 3 chunks
| Chunk | Items | Partial sum |
|---|---|---|
| 0 | 5, 8, 3, 9 | 25 |
| 1 | 4, 7, 6, 2 | 19 |
| 2 | 10, 1, 8, 5 | 24 |
| Merge | add the partial sums | 68 |
The cells students fill in are left blank in this preview.
Run the split and the merge
# Cut 12 order totals into 3 chunks, add each chunk, then merge.
totals = [5, 8, 3, 9, 4, 7, 6, 2, 10, 1, 8, 5]
chunks = [totals[0:4], totals[4:8], totals[8:12]]
partials = []
for chunk in chunks:
partials.append(sum(chunk))
print("chunks:", chunks)
print("partial sums:", partials)
print("merged:", sum(partials), "in one piece:", sum(totals))
# Cut the list into 2 chunks of 6 instead, then run again.
Processes have their own memory, threads share it
A process is a running program with its own memory. A thread runs inside a process and shares that memory.
Standard Python lets only one thread run Python code at a time. So for work that needs the processor, Python programs use several processes. Each has its own copy of the data and its own answer to send back.
A pool keeps the chunks in order
The lab's chunks.py cuts the numbers 1 to 40 into four chunks of ten and gives them to a pool of two workers. A worker that finishes takes the next chunk.
The partial sums come back in chunk order, 55, 155, 255 and 355, whichever worker finished first. Their sum is 820, the same as summing the whole list in one piece.
Run the chunked sum
docker exec jupyter python3 steps/chunks.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Cut ten numbers into two chunks
docker exec jupyter python3 -c "d = list(range(1, 11)); print(d[0:5], d[5:10])"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Not every answer merges by adding
A sum merges by adding. An average does not: the average of the chunk averages is wrong when the chunks differ in size.
Each chunk must send back what the merge needs. For an average that is the sum and the count. The example below shows the wrong and the right way.
Run the two ways to merge
# The average of chunks is NOT the average of the chunk averages.
chunk_a = [10, 20, 30, 40]
chunk_b = [100]
wrong = (sum(chunk_a) / len(chunk_a) + sum(chunk_b) / len(chunk_b)) / 2
right = (sum(chunk_a) + sum(chunk_b)) / (len(chunk_a) + len(chunk_b))
print("average of the averages:", wrong)
print("sum and count merged:", right)
A common mistake
Warning
Many people think splitting a job is free. Cutting the data and merging the answers are serial work, and a process needs its copy of the data. Too many tiny chunks can cost more than they save, so keep each chunk big enough to be worth sending.