В Python-скрипте, использующем multiprocessing.Manager() для обмена словарями между процессами, наблюдается взаимная блокировка — приведите минимальный пример кода, объясните причину дедлока и предложите корректный способ организации обмена данными
Пример минимального кода, приводящего к дедлоку (две RLock берутся в разном порядке): ```python from multiprocessing import Process, Manager from time import sleep def p1(l1, l2, d1): with l1: # захват первой блокировки sleep(0.1) # имитируем работу with l2: # ждём вторую — может привести к дедлоку d1['p1'] = 'ok' def p2(l1, l2, d2): with l2: # захват во втором процессе в обратном порядке sleep(0.1) with l1: d2['p2'] = 'ok' if __name__ == '__main__': mgr = Manager() d1 = mgr.dict() d2 = mgr.dict() l1 = mgr.RLock() l2 = mgr.RLock() proc1 = Process(target=p1, args=(l1, l2, d1)) proc2 = Process(target=p2, args=(l1, l2, d2)) proc1.start() proc2.start() proc1.join() # программа зависнет здесь из‑за взаимной блокировки proc2.join() ``` Причина дедлока (кратко): - Manager создаёт серверные объекты (прокси) для словарей и замков. Каждый `RLock` — отдельный синхронизируемый ресурс в сервере. - Процесс A берёт `l1` и ждёт `l2`, в то же время процесс B берёт `l2` и ждёт `l1` — классический deadlock из‑за разного порядка захвата ресурсов. Корректные способы организации обмена данными (рекомендации и пример): 1) Использовать очереди для обмена сообщениями (рекомендуется). Очереди проще и меньше подвержены дедлокам: ```python from multiprocessing import Process, Queue def p1(q_send, q_recv): q_send.put('message from p1') msg = q_recv.get() # блокируемся, но обмен упорядочен через очереди print('p1 got', msg) def p2(q_send, q_recv): q_send.put('message from p2') msg = q_recv.get() print('p2 got', msg) if __name__ == '__main__': q1 = Queue() # очередь для сообщений в p1 q2 = Queue() # очередь для сообщений в p2 a = Process(target=p1, args=(q2, q1)) # p1 пишет в q2, читает из q1 b = Process(target=p2, args=(q1, q2)) a.start(); b.start() a.join(); b.join() ``` 2) Если всё же нужны общие структуры: - Соблюдать единый порядок захвата замков во всех процессах (глобальный порядок ресурсов). - Или избегать больших критических секций: читать/писать быстро и не держать замок во время долгой работы. - Использовать неблокирующие попытки с таймаутом и повторной попыткой (backoff) для предотвращения зависания. 3) Альтернативы: - Возвращать результаты через Pool/Process (через return / Queue), использовать shared memory (`multiprocessing.Value`/`Array`) для простых числовых данных, или специализированные библиотеки (Redis, ZeroMQ) для сложного обмена. Короткое резюме: дедлок возникает из‑за захвата нескольких менеджерных замков в разном порядке; проще и надежнее обмениваться сообщениями через `multiprocessing.Queue` или обеспечить строгий единый порядок захвата ресурсов.
```python
from multiprocessing import Process, Manager
from time import sleep
def p1(l1, l2, d1):
with l1: # захват первой блокировки
sleep(0.1) # имитируем работу
with l2: # ждём вторую — может привести к дедлоку
d1['p1'] = 'ok'
def p2(l1, l2, d2):
with l2: # захват во втором процессе в обратном порядке
sleep(0.1)
with l1:
d2['p2'] = 'ok'
if __name__ == '__main__':
mgr = Manager()
d1 = mgr.dict()
d2 = mgr.dict()
l1 = mgr.RLock()
l2 = mgr.RLock()
proc1 = Process(target=p1, args=(l1, l2, d1))
proc2 = Process(target=p2, args=(l1, l2, d2))
proc1.start()
proc2.start()
proc1.join() # программа зависнет здесь из‑за взаимной блокировки
proc2.join()
```
Причина дедлока (кратко):
- Manager создаёт серверные объекты (прокси) для словарей и замков. Каждый `RLock` — отдельный синхронизируемый ресурс в сервере.
- Процесс A берёт `l1` и ждёт `l2`, в то же время процесс B берёт `l2` и ждёт `l1` — классический deadlock из‑за разного порядка захвата ресурсов.
Корректные способы организации обмена данными (рекомендации и пример):
1) Использовать очереди для обмена сообщениями (рекомендуется). Очереди проще и меньше подвержены дедлокам:
```python
from multiprocessing import Process, Queue
def p1(q_send, q_recv):
q_send.put('message from p1')
msg = q_recv.get() # блокируемся, но обмен упорядочен через очереди
print('p1 got', msg)
def p2(q_send, q_recv):
q_send.put('message from p2')
msg = q_recv.get()
print('p2 got', msg)
if __name__ == '__main__':
q1 = Queue() # очередь для сообщений в p1
q2 = Queue() # очередь для сообщений в p2
a = Process(target=p1, args=(q2, q1)) # p1 пишет в q2, читает из q1
b = Process(target=p2, args=(q1, q2))
a.start(); b.start()
a.join(); b.join()
```
2) Если всё же нужны общие структуры:
- Соблюдать единый порядок захвата замков во всех процессах (глобальный порядок ресурсов).
- Или избегать больших критических секций: читать/писать быстро и не держать замок во время долгой работы.
- Использовать неблокирующие попытки с таймаутом и повторной попыткой (backoff) для предотвращения зависания.
3) Альтернативы:
- Возвращать результаты через Pool/Process (через return / Queue), использовать shared memory (`multiprocessing.Value`/`Array`) для простых числовых данных, или специализированные библиотеки (Redis, ZeroMQ) для сложного обмена.
Короткое резюме: дедлок возникает из‑за захвата нескольких менеджерных замков в разном порядке; проще и надежнее обмениваться сообщениями через `multiprocessing.Queue` или обеспечить строгий единый порядок захвата ресурсов.