1. Lock (блокировка)
Синхронизация в Python: Lock, Semaphore и Queue
В многопоточных приложениях одновременный доступ к общим ресурсам может привести к состоянию гонки (race condition), повреждению данных или недетерминированному поведению. Для решения этих проблем в Python предоставляются механизмы синхронизации: Lock, Semaphore и Queue. Рассмотрим каждый из них.
1. Lock (блокировка)
Lock — базовый примитив синхронизации, который позволяет обеспечить эксклюзивный доступ к ресурсу. Только один поток может захватить блокировку, остальные ждут ее освобождения.
Пример проблемы без Lock
import threading
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print("Counter:", counter) # Результат может быть меньше 500000 из-за гонки данных.
Использование Lock
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # Автоматическое захват и освобождение
counter += 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print("Counter:", counter) # Всегда 500000
Когда использовать: Для защиты критических участков кода, где важен эксклюзивный доступ (например, изменение общей переменной).
2. Semaphore (семафор)
Semaphore — это счетчик, ограничивающий количество потоков, которые могут одновременно получить доступ к ресурсу. Если семафор инициализирован значением N, то одновременно работать с ресурсом могут N потоков.
Пример использования
import threading
import time
semaphore = threading.Semaphore(3) # Одновременно могут работать 3 потока
def access_resource(thread_id):
with semaphore:
print(f"Поток {thread_id} получил доступ")
time.sleep(2)
print(f"Поток {thread_id} освободил доступ")
threads = []
for i in range(10):
t = threading.Thread(target=access_resource, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
Результат: Первые три потока получают доступ сразу, остальные ждут освобождения семафора.
Когда использовать: Для ограничения доступа к ресурсам с ограниченной пропускной способностью (например, подключения к базе данных).
3. Queue (очередь)
Queue — потокобезопасная структура данных для обмена сообщениями между потоками. Она автоматически управляет блокировками, что упрощает реализацию шаблонов типа «Производитель-Потребитель».
Пример Producer-Consumer
import threading
import queue
import time
q = queue.Queue(maxsize=5) # Очередь максимум на 5 элементов
def producer():
for i in range(10):
q.put(i)
print(f"Произведено: {i}")
time.sleep(0.5)
def consumer():
while True:
item = q.get()
if item is None: # Сигнал завершения
break
print(f"Потреблено: {item}")
q.task_done()
# Запуск потоков
prod_thread = threading.Thread(target=producer)
cons_thread = threading.Thread(target=consumer)
prod_thread.start()
cons_thread.start()
prod_thread.join()
q.put(None) # Отправка сигнала завершения потребителю
cons_thread.join()
Особенности:
- Методы
put()иget()блокируют поток, если очередь заполнена или пуста. task_done()иjoin()используются для отслеживания завершения задач.
Когда использовать: Для безопасной передачи данных между потоками (например, обработка задач в фоне).
Сравнение и рекомендации
| Инструмент | Назначение | Пример использования |
|---|---|---|
| Lock | Эксклюзивный доступ к ресурсу | Изменение общей переменной |
| Semaphore | Ограничение доступа N потоков | Пул подключений к базе данных |
| Queue | Безопасный обмен данными | Многопоточная обработка задач |
Заключение
- Lock защищает критические участки кода.
- Semaphore ограничивает количество одновременных операций.
- Queue упрощает обмен данными между потоками.
Выбор инструмента зависит от задачи: используйте Lock для синхронизации, Semaphore для ограничения доступа и Queue для передачи данных. Все эти механизмы помогают избежать ошибок в многопоточных приложениях.