Interprocess communication and how to not shoot yourself in the foot
CNRS
IMAG
Paul-Valéry Montpellier 3 University
Main Process
┌─────────────┐
│ │
│ CPU │
├─────────────┤
│ │
│ Memory │
└─┬────┬────┬─┘
│ │ │
│ │ │
┌───────────┘ │ └───────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ │ │ │ │ │
│ CPU │ │ CPU │ │ CPU │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ │ │ │ │ │
│ Memory │ │ Memory │ │ Memory │
└─────────────┘ └─────────────┘ └─────────────┘
Process 1 Process 1 Process 1
┌──────────────────────────────────────┐
│ MAIN PROCESS │
│ │
│ │
│ ┌──────────┐ │
│ │ │ │
│ │ CPU │ ┌───────────┐ │
│ │ │ │ │ │
│ └──────────┘ │ Memory │ │
│ │ │ │
│ └───────────┘ │
│ │
│ │
│ │
│ ┌─┐ ┌─┐ ┌─┐ │
│ │┼│ │┼│ │┼│ │
│ │┴│ │┴│ │┴│ │
│ ▼▼▼ ▼▼▼ ▼▼▼ │
│ Thread 1 Thread 2 Thread 3 │
│ ┌─┐ ┌─┐ ┌─┐ │
│ │┼│ │┼│ │┼│ │
│ │┴│ │┴│ │┴│ │
│ ▼▼▼ ▼▼▼ ▼▼▼ │
│ │
└──────────────────────────────────────┘
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ ┌──────────┐ ┌──────────┐ │ │ ┌──────────┐ ┌──────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ CORE 1 │ │ CORE 2 │ │ │ │ CORE 3 │ │ CORE 4 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └─┬──┬─────┘ └────┬─────┘ │ │ └┬─────────┘ └──────┬───┘ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ CPU 1 │ │ │ │ CPU 2 │ │
│ │ │ │ │ │ │ │ │
└───┼──┼──────────────┼───────┘ └──┼────────────────────┼─────┘
│ │ │ │ │
│ │ └────────────┐ │ │
│ │ │ │ │
│ └─────────────────────────┐ │ │ │
│ │ │ │ ┌─────────────────┘
└──────────────────────────┐ │ │ │ │
│ │ │ │ │
┌──────────────────────────────┼─┼─┼──┼──┼──────────────────────┐
│ │ │ │ │ │ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─▼─▼─▼──▼──▼─┐ │
│ │ │ │ │ │ │ │Shared Memory│ │
│ └─────┘ └─────┘ └─────┘ └─────────────┘ │
│ Main Memory │
└───────────────────────────────────────────────────────────────┘
There are differents models
An ubiquitous tool in multiprocessing (and distributed computing) is shared memory FIFO
list, aka Queues.
A FIFO is a :
enqueue(x)
et dequeue()
function (or push(x)
/pop()
)In the context of multi-processing (or multi-threading) :
Shared Memory + FIFO list = Queue
Queues are the basis of the consumer/producer model, which is widely used in concurrent and distributed applications.
An algorithm with two computations A and B where :
A could be a producer for B, and B a consumer for A.
┌───────────┐
│ │
│ Producer │
│ │ Process A
│ │
└─────┬─────┘
│
┌────┼───────────────────────────────────────────────────────────────────┐
│ │ Queue │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ │ │ │ │ │ │ │ │ │
│ └───────►│ │ │ │ │ │ │ ├──────────┐ │
│ │ │ │ │ │ │ │ │ │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │
│ │ │
│ Shared Memory │ │
└──────────────────────────────────────────────────────────────────┼─────┘
│
▼
┌───────────┐
│ │
Process B │ Consumer │
│ │
│ │
└───────────┘
what if several processes want to write/read the same shared memory portions at the same time?
Enter the realm of the dreaded race condition
Printing from several processes a string with 10 times the same char.
Output:
AAAAAAAAAACCCCCCCCCCBBBBBBBBBBDDDDDDDDDDEEEEEEEEEE
FFFFFFFFFFGGGGGGGGGG
IIIIIIIIII
HHHHHHHHHH
JJJJJJJJJJ
A critical section is :
┌─────────────┐
│ │
│ Normal │
│ Code │ Parallelized
│ │
└──────┬──────┘
│
┌──────▼──────┐
│ │
│ Critical │ Not parallelized
│ Section │
│ │
└──────┬──────┘
│
┌──────▼──────┐
│ │
│ Normal │ Parallelized
│ Code │
│ │
└─────────────┘
from multiprocessing.pool import Pool
from multiprocessing import Lock
from itertools import repeat
lock = Lock()
def safe_repeat10Cap(c):
with lock:
# Beginning of critical section
print("".join(repeat(chr(c+65),10)))
# End of critical section
with Pool(8) as pool:
pool.map(safe_repeat10Cap, range(10))
Output:
AAAAAAAAAA
BBBBBBBBBB
CCCCCCCCCC
DDDDDDDDDD
EEEEEEEEEE
FFFFFFFFFF
GGGGGGGGGG
HHHHHHHHHH
IIIIIIIIII
JJJJJJJJJJ
Process A (resp. B) wants to push
x (resp. y) on the list.
Process A and B both want to pop
the list.
Warning
⚠ ⚠ As long the list is not empty ⚠ ⚠
Beware of putting locks everywhere… Beware…
Process A acquires lock L1. Process B acquires lock L2. Process A tries to acquire lock L2, but it is already held by B. Process B tries to acquire lock L1, but it is already held by A. Both processes are blocked.
There is several ways to avoid deadlocks. One of them is the Dijkstra’s Resource Hiearchy Solution.
In the previous example, processes should try the lowest numbered locks first. Instead of B acquiring L2 first, it should tries to acquire L1 instead and L2 after.
This solution isn’t universal but is pretty usable in general case.
Diving (a little) deeper into parallelism, when computations are NOT independent of each other (no embarrasingly parallel approach), we need a way to decouple processing of data, while still keeping the dependancies intact.
\Longrightarrow Shared Memory and Queues to the rescue
With the concurrent use of ressources, there are two pitfalls to be aware of:
IPC and lockingAdvanced Programming and Parallel Computing, Master 2 MIASHS