This commit is contained in:
lovinervy committed 2026-10-07 13:06:47 +05:00
commit c394677d07
59 files changed
+5149

No files matched your search

+69
View File
@@ -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()
+56
View File
@@ -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()
+46
View File
@@ -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()
+98
View File
@@ -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()
+115
View File
@@ -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` в другом порядке → дедлок
(порядок взятия замков должен быть единым).