Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: гонка данных — потерянные обновления счётчика.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab05_sync/examples/01_race_demo.py
|
||||
|
||||
counter += 1 — три операции (чтение, инкремент, запись). Два потока читают
|
||||
одно значение, оба пишут — одно увеличение теряется. Lock устраняет гонку.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
N_THREADS = 8
|
||||
N_INCR = 100_000
|
||||
|
||||
|
||||
def worker_unsafe(counter):
|
||||
for _ in range(N_INCR):
|
||||
value = counter[0] # ЧТЕНИЕ
|
||||
time.sleep(0) # усиливает переключения: GIL уступается
|
||||
counter[0] = value + 1 # ЗАПИСЬ (могла потеряться)
|
||||
|
||||
|
||||
def worker_safe(counter, lock):
|
||||
for _ in range(N_INCR):
|
||||
with lock: # критическая секция
|
||||
value = counter[0]
|
||||
counter[0] = value + 1
|
||||
|
||||
|
||||
def main():
|
||||
expected = N_THREADS * N_INCR
|
||||
|
||||
counter = [0]
|
||||
threads = [threading.Thread(target=worker_unsafe, args=(counter,))
|
||||
for _ in range(N_THREADS)]
|
||||
t0 = time.perf_counter()
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
unsafe_time = time.perf_counter() - t0
|
||||
print(f"без синхронизации: {counter[0]:>10,} из {expected:,} "
|
||||
f"(потеряно {expected - counter[0]:,}) за {unsafe_time:.2f} c")
|
||||
|
||||
counter = [0]
|
||||
lock = threading.Lock()
|
||||
threads = [threading.Thread(target=worker_safe, args=(counter, lock))
|
||||
for _ in range(N_THREADS)]
|
||||
t0 = time.perf_counter()
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
safe_time = time.perf_counter() - t0
|
||||
print(f"с Lock: {counter[0]:>10,} из {expected:,} "
|
||||
f"(идеально) за {safe_time:.2f} c")
|
||||
print(f"\nРазница времени: {(safe_time - unsafe_time) / unsafe_time * 100:+.0f}%")
|
||||
print("(Lock может даже ускорить: без него потоки чаще конфликтуют)")
|
||||
print("Запустите ещё раз — потерянные обновления будут другими:")
|
||||
print("гонка недетерминирована, этим она и опасна.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: Semaphore — лимит одновременного доступа к ресурсу.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab05_sync/examples/02_semaphore_demo.py
|
||||
|
||||
Имитация: 10 «клиентов», ресурс допускает 3 одновременных подключения.
|
||||
Семафор гарантирует: активных клиентов никогда не больше 3.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
MAX_CONN = 3
|
||||
N_CLIENTS = 10
|
||||
DELAY = 0.2
|
||||
|
||||
active = 0
|
||||
active_lock = threading.Lock()
|
||||
max_seen = [0]
|
||||
|
||||
|
||||
def client(idx, sem):
|
||||
global active
|
||||
with sem: # ждём свободный «слот»
|
||||
with active_lock:
|
||||
active += 1
|
||||
max_seen[0] = max(max_seen[0], active)
|
||||
print(f" клиент {idx:>2} подключился (активно: {active})")
|
||||
time.sleep(DELAY) # «работа с ресурсом»
|
||||
with active_lock:
|
||||
active -= 1
|
||||
print(f" клиент {idx:>2} отключился (активно: {active})")
|
||||
|
||||
|
||||
def main():
|
||||
sem = threading.Semaphore(MAX_CONN)
|
||||
threads = [threading.Thread(target=client, args=(i, sem))
|
||||
for i in range(N_CLIENTS)]
|
||||
t0 = time.perf_counter()
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
total = time.perf_counter() - t0
|
||||
|
||||
print(f"\nмаксимум одновременно: {max_seen[0]} (лимит {MAX_CONN})")
|
||||
print(f"время: {total:.2f} c, идеал ≈ {N_CLIENTS / MAX_CONN * DELAY:.2f} c")
|
||||
print("Если max_seen > лимита — семафор сломан (такого быть не должно).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: Event — сигнал между потоками.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab05_sync/examples/03_event_demo.py
|
||||
|
||||
Потоки-«работники» ждут сигнала готовности (Event.set()), после чего
|
||||
начинают работу одновременно. Event — как стартовый пистолет.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
|
||||
def worker(idx, start_event, results):
|
||||
name = f"работник-{idx}"
|
||||
print(f" {name}: готов, жду сигнала")
|
||||
start_event.wait() # блокируется, пока set() не вызван
|
||||
t0 = time.perf_counter()
|
||||
time.sleep(0.1) # «работа»
|
||||
results[idx] = (name, time.perf_counter() - t0)
|
||||
print(f" {name}: закончил")
|
||||
|
||||
|
||||
def main():
|
||||
start = threading.Event()
|
||||
results = {}
|
||||
threads = [threading.Thread(target=worker, args=(i, start, results))
|
||||
for i in range(4)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
|
||||
time.sleep(0.5) # «подготовка данных»
|
||||
print("главный: сигнал! Event.set()")
|
||||
start.set() # все ждущие просыпаются РАЗОМ
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
print(f"все закончили: {len(results)} из {len(threads)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Лабораторная работа 5: синхронизация и гонки данных.
|
||||
|
||||
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||
автотесты (tests/test_lab05.py).
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab05_sync/solution.py
|
||||
"""
|
||||
import os
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from common.tasks import io_fetch
|
||||
|
||||
# TODO: впишите свой номер в журнале (1..20) — используется в main()
|
||||
VARIANT_NUMBER = 0
|
||||
|
||||
|
||||
def race_demo(n_threads: int, n_increments: int):
|
||||
"""Гонка данных БЕЗ синхронизации.
|
||||
|
||||
n_threads потоков, каждый увеличивает общий счётчик n_increments раз.
|
||||
counter += 1 не атомарен! Чтобы гонка проявилась надёжно, после чтения
|
||||
счётчика добавьте time.sleep(0) — уступает GIL.
|
||||
Вернуть (фактический_счётчик, ожидаемый = n_threads * n_increments).
|
||||
"""
|
||||
# TODO: общий счётчик (список [0] или nonlocal), потоки, start/join
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def race_fixed(n_threads: int, n_increments: int):
|
||||
"""То же, но с threading.Lock. Вернуть (фактический_счётчик, ожидаемый).
|
||||
|
||||
Результат обязан быть точным ПРИ ЛЮБЫХ запусках.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def semaphore_limiter(items: int, delay: float, max_concurrent: int):
|
||||
"""Выполнить items запросов io_fetch(item, delay) на потоках,
|
||||
но не более max_concurrent одновременно (threading.Semaphore).
|
||||
|
||||
Вернуть (список_результатов, elapsed_seconds), упорядочен по item.
|
||||
Подсказка: results[idx] — каждый воркер пишет в СВОЮ ячейку списка.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def producer_consumer(n_producers: int, n_consumers: int, n_items: int):
|
||||
"""Схема producer-consumer на queue.Queue.
|
||||
|
||||
Производители суммарно кладут задачи со значениями 0..n_items-1
|
||||
(распределите между производителями как удобно, но все значения ровно
|
||||
по одному разу). Потребители забирают и «обрабатывают» (суммируют).
|
||||
Программа обязана завершиться сама: маркеры конца (None) — по одному
|
||||
на потребителя. Вернуть (обработано_задач, сумма_значений).
|
||||
"""
|
||||
# TODO: queue.Queue() для задач и результатов; producers кладут;
|
||||
# consumers берут до None; join всех; собрать результаты
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def main():
|
||||
print("=== 1. Гонка данных ===")
|
||||
for attempt in range(3):
|
||||
got, expected = race_demo(8, 100_000)
|
||||
print(f"попытка {attempt + 1}: {got:,} из {expected:,} "
|
||||
f"(потеряно {expected - got:,})")
|
||||
got, expected = race_fixed(8, 100_000)
|
||||
print(f"с Lock: {got:,} из {expected:,} (всегда точно)")
|
||||
|
||||
print("\n=== 2. Семафор: лимит соединений ===")
|
||||
v_items, v_delay = 24, 0.05
|
||||
for mc in (2, 4, 8):
|
||||
res, t = semaphore_limiter(v_items, v_delay, mc)
|
||||
ideal = v_items / mc * v_delay
|
||||
print(f"max_concurrent={mc}: {len(res)} запросов за {t:.2f} c "
|
||||
f"(идеал ≈ {ideal:.2f} c)")
|
||||
|
||||
print("\n=== 3. Producer–consumer ===")
|
||||
t0 = time.perf_counter()
|
||||
processed, total_sum = producer_consumer(2, 3, 30)
|
||||
print(f"производителей=2, потребителей=3, задач=30: "
|
||||
f"обработано {processed}, сумма {total_sum}, "
|
||||
f"время {time.perf_counter() - t0:.2f} c")
|
||||
print("Если программа зависла — дедлок: проверьте маркеры конца!")
|
||||
|
||||
# TODO: в отчёт — несколько запусков race_demo (недетерминизм!),
|
||||
# цена Lock, таблицу semaphore_limiter, схему producer-consumer.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
# Лабораторная работа 5. Синхронизация и гонки данных
|
||||
|
||||
**Время на работу:** 2 пары. **Зависимости:** лабы 2, 4.
|
||||
|
||||
## Цель работы
|
||||
|
||||
Увидеть гонку данных своими глазами (без синхронизации результат «плавает»),
|
||||
научиться защищать общие ресурсы (`Lock`, `Semaphore`, `Event`) и собрать
|
||||
классическую схему producer–consumer без дедлоков.
|
||||
|
||||
## Теория
|
||||
|
||||
### Гонка данных
|
||||
|
||||
Гонка — ситуация, когда результат зависит от порядка доступа потоков к общим
|
||||
данным. Классика: `counter += 1` — это **три** операции (прочитать,
|
||||
увеличить, записать). Два потока могут прочитать одно значение, оба
|
||||
увеличить, оба записать — одно из двух увеличений потеряется.
|
||||
|
||||
Важно: `x += 1` в Python **не атомарен**. Атомарны отдельные операции вроде
|
||||
`list.append` (спорно, но под GIL обычно надёжно) — полагаться на это нельзя.
|
||||
|
||||
### Инструменты синхронизации
|
||||
|
||||
| Инструмент | Что делает | Когда нужен |
|
||||
|---|---|---|
|
||||
| `Lock` | взаимное исключение: только один в критической секции | защита счётчиков, списков |
|
||||
| `RLock` | Lock, который можно взять повторно тем же потоком | рекурсивные функции |
|
||||
| `Semaphore(n)` | разрешает не более n одновременных входов | лимит на соединения/ресурс |
|
||||
| `Event` | флаг «событие произошло»: `set()` / `wait()` | сигнал между потоками |
|
||||
| `Condition` | ожидание условия (`wait`/`notify`) | сложные очереди |
|
||||
| `Barrier(n)` | все ждут, пока соберутся n | синхронный старт |
|
||||
|
||||
В multiprocessing те же примитивы есть в `multiprocessing` (они работают
|
||||
между процессами через разделяемую память ОС).
|
||||
|
||||
### Producer–consumer
|
||||
|
||||
Классическая схема: производители кладут задачи в очередь, потребители
|
||||
забирают. `queue.Queue` — потокобезопасна сама по себе (внутри Lock +
|
||||
Condition). Главные грабли:
|
||||
|
||||
- **Дедлок**: потребитель ждёт `get()`, а производитель уже закончил.
|
||||
Решение: «маркер конца» (`put(None)`) — по одному на каждого потребителя,
|
||||
или `get(timeout=...)`/`get_nowait()`.
|
||||
- **Забытый join** очереди или потоков — программа завершается раньше времени.
|
||||
|
||||
### Как искать гонки
|
||||
|
||||
- Замеры не помогут — гонка «мимикрирует». Помогает:
|
||||
1. увеличение числа потоков и повторов;
|
||||
2. `time.sleep(0)` внутри критической секции — усиливает переключения;
|
||||
3. сравнение результата с эталоном (checksum).
|
||||
|
||||
## Задание
|
||||
|
||||
Заполните `solution.py`:
|
||||
|
||||
1. **`race_demo(n_threads, n_increments)`** — n потоков увеличивают общий
|
||||
счётчик `n_increments` раз каждый БЕЗ синхронизации. Вернуть
|
||||
`(фактический_счётчик, ожидаемый)`. Фактический должен быть МЕНЬШЕ
|
||||
ожидаемого хотя бы иногда — если всегда ровно, увеличьте параметры
|
||||
и повторите (см. «Как искать гонки»).
|
||||
2. **`race_fixed(n_threads, n_increments)`** — то же с `threading.Lock`.
|
||||
Результат обязан быть точным всегда. Замерьте, во что обходится Lock.
|
||||
3. **`semaphore_limiter(items, delay, max_concurrent)`** — выполнить
|
||||
`items` «запросов» `io_fetch` на потоках, но не более
|
||||
`max_concurrent` одновременно (`Semaphore` + ThreadPoolExecutor).
|
||||
Вернуть `(результаты, сек)`.
|
||||
4. **`producer_consumer(n_producers, n_consumers, n_items)`** — производители
|
||||
кладут `n_items` задач суммарно, потребители обрабатывают, программа
|
||||
завершается без дедлока. Вернуть количество обработанных задач.
|
||||
5. **`main()`** — демонстрация всех пункто�� с замерами.
|
||||
|
||||
### Ожидаемые результаты
|
||||
|
||||
- `race_demo`: счётчик < ожидаемого (часто сильно); при больших
|
||||
`n_increments` потери растут.
|
||||
- `race_fixed`: всегда ровно; время чуть больше (цена Lock).
|
||||
- `semaphore_limiter`: время ≈ `items / max_concurrent × delay` — лимит
|
||||
работает.
|
||||
- `producer_consumer`: обработано ровно `n_items`, завершается сам.
|
||||
|
||||
## Контрольные вопросы
|
||||
|
||||
1. Почему `counter += 1` не атомарен? Какие операции он включает?
|
||||
2. Чем `Semaphore(3)` отличается от трёх `Lock`? Где это полезно?
|
||||
3. Что такое дедлок и как «маркер конца» его предотвращает?
|
||||
4. Почему `queue.Queue` не требует внешнего Lock?
|
||||
5. Приведите пример из вашей CPU-задачи, где нужна синхронизация при
|
||||
многопоточности (например, сборка результатов).
|
||||
|
||||
## Что сдаётся
|
||||
|
||||
`solution.py` + отчёт: вывод race_demo (несколько запусков!), сравнение
|
||||
race_fixed, замеры semaphore_limiter, описание схемы producer–consumer,
|
||||
ответы на вопросы.
|
||||
|
||||
## Критерии оценки
|
||||
|
||||
| Пункт | Баллы |
|
||||
|---|---|
|
||||
| race_demo демонстрирует потери (или обоснование, почему не демонстрирует) | 2 |
|
||||
| race_fixed даёт точный результат всегда | 2 |
|
||||
| semaphore_limiter: лимит соблюдается, замеры есть | 2 |
|
||||
| producer–consumer завершается сам, ничего не теряется | 2 |
|
||||
| Отчёт и ответы на вопросы | 2 |
|
||||
|
||||
## Типичные ошибки
|
||||
|
||||
- race_demo всегда даёт точный результат → увеличьте n_increments (10⁵+),
|
||||
число потоков, добавьте `time.sleep(0)` после чтения счётчика.
|
||||
- producer–consumer висит → забыли маркеры конца или не join() потоки.
|
||||
- `Lock` взят внутри `with` другого `Lock` в другом порядке → дедлок
|
||||
(порядок взятия замков должен быть единым).
|
||||
Reference in new issue
Block a user