Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,86 @@
|
|||||||
|
# Технология параллельного программирования (Python)
|
||||||
|
|
||||||
|
Курс лабораторных работ для направления «Прикладная математика и информатика».
|
||||||
|
|
||||||
|
## Цель курса
|
||||||
|
|
||||||
|
Научиться осознанно выбирать модель исполнения для конкретной задачи:
|
||||||
|
|
||||||
|
| Модель | Когда помогает | Инструменты |
|
||||||
|
|---|---|---|
|
||||||
|
| Последовательный код | базовая линия | stdlib |
|
||||||
|
| Потоки (threads) | задачи I/O-bound | `threading`, `concurrent.futures` |
|
||||||
|
| Процессы (processes) | задачи CPU-bound | `multiprocessing` |
|
||||||
|
| Асинхронность | много I/O-операций, кооперативность | `asyncio` |
|
||||||
|
| Распределённые вычисления | несколько машин / узлов | `mpi4py` (MS-MPI) |
|
||||||
|
|
||||||
|
Отдельное внимание: **GIL** — почему потоки в CPython не ускоряют чистый Python-код,
|
||||||
|
и как это обходить.
|
||||||
|
|
||||||
|
## Состав работ
|
||||||
|
|
||||||
|
| № | Папка | Тема |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `lab00_intro/` | Окружение: Python, venv, VS Code, проверка готовности |
|
||||||
|
| 1 | `lab01_sequential/` | Последовательное вычисление, профилирование, CPU-bound vs I/O-bound |
|
||||||
|
| 2 | `lab02_threads/` | Многопоточность на I/O-bound задачах, `ThreadPoolExecutor` |
|
||||||
|
| 3 | `lab03_gil/` | GIL: почему потоки не ускоряют CPU-код; `hashlib` как исключение |
|
||||||
|
| 4 | `lab04_multiprocessing/` | Процессы, `Pool`, накладные расходы, spawn на Windows |
|
||||||
|
| 5 | `lab05_sync/` | Гонки данных, `Lock`, `Semaphore`, `Event`, producer–consumer |
|
||||||
|
| 6 | `lab06_asyncio/` | `async/await`, event loop, `gather`, семафор, сравнение с потоками |
|
||||||
|
| 7 | `lab07_comparison/` | Одна задача × 4 подхода, закон Амдала, итоговые графики |
|
||||||
|
| 8 | `lab08_mpi/` | Распределённые вычисления, MPI (по указанию преподавателя) |
|
||||||
|
|
||||||
|
## Варианты
|
||||||
|
|
||||||
|
Каждый студент выполняет **свой вариант** — см. `common/варианты.md`
|
||||||
|
(20 вариантов: CPU-задача + параметры + I/O-нагрузка). Ядра всех задач уже
|
||||||
|
реализованы в `common/tasks.py` — смысл лабораторных именно в *организации
|
||||||
|
вычислений и замерах*, а не в переписывании алгоритмов.
|
||||||
|
|
||||||
|
## Установка (Windows, 10/11)
|
||||||
|
|
||||||
|
1. Python 3.10+ с python.org (галочка **Add python.exe to PATH**).
|
||||||
|
2. Открыть папку курса в VS Code (расширение Python).
|
||||||
|
3. В терминале VS Code:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python lab00_intro\check_env.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вывод кириллицы «кракозябрами» — выполните `chcp 65001`.
|
||||||
|
|
||||||
|
На macOS/Linux всё то же самое, кроме `activate` (там `source .venv/bin/activate`).
|
||||||
|
|
||||||
|
## Как сдавать
|
||||||
|
|
||||||
|
- Код: `labXX_.../solution.py` (заполняются заготовки, интерфейсы менять нельзя —
|
||||||
|
по ним работают автотесты).
|
||||||
|
- Отчёт: по шаблону `common/шаблон_отчёта.md` — таблицы замеров, графики, выводы.
|
||||||
|
- Самопроверка перед сдачей: `python -m pytest tests/ -k labXX` из корня курса.
|
||||||
|
- Чек-лист: `common/чек-лист_сдачи.md`. Типичные ошибки: `common/типичные_ошибки.md`.
|
||||||
|
|
||||||
|
## Структура репозитория
|
||||||
|
|
||||||
|
```
|
||||||
|
common/ банк задач, варианты, утилиты замеров, шаблоны
|
||||||
|
lab00_intro/ ... lab08_mpi/
|
||||||
|
методичка.md теория + задание + контрольные вопросы + критерии
|
||||||
|
examples/ запускаемые демонстрации
|
||||||
|
solution.py заготовка решения (заполняет студент)
|
||||||
|
tests/ автотесты pytest (самопроверка студента, проверка преподавателя)
|
||||||
|
instructor_solutions/ эталонные решения (только для преподавателя)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск примеров и решений
|
||||||
|
|
||||||
|
Все команды выполняются **из корня курса**:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
python lab01_sequential\examples\01_perf_counter_demo.py
|
||||||
|
python lab01_sequential\solution.py
|
||||||
|
python -m pytest tests\ -v
|
||||||
|
```
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Банк CPU-задач для вариантов
|
||||||
|
|
||||||
|
Все ядра уже реализованы в `common/tasks.py`. Здесь — смысловые описания:
|
||||||
|
что считается результатом, почему задача CPU-bound и чем интересна при
|
||||||
|
распараллеливании. Единый интерфейс каждой задачи:
|
||||||
|
|
||||||
|
```
|
||||||
|
split(total, parts) -> куски — разбиение работы на части
|
||||||
|
kernel(кусок) -> частичный результат — вычисление одной части
|
||||||
|
combine(части) -> итог — сборка итога
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот интерфейс намеренно совпадает с типовой схемой MapReduce — он один и тот же
|
||||||
|
во всех лабораторных: сначала вы вызываете его последовательно, затем потоками,
|
||||||
|
процессами и т. д.
|
||||||
|
|
||||||
|
| № | Задача | Результат | Почему CPU-bound | Особенности при распараллеливании |
|
||||||
|
|---|--------|-----------|------------------|-----------------------------------|
|
||||||
|
| 1 | Умножение матриц (наивное, N×N) | контрольная сумма произведения | тройной цикл на чистом Python | разбиение по строкам; большие данные при пересылке |
|
||||||
|
| 2 | Число π методом Монте-Карло | приближение π | миллионы случайных бросков | идеально независимые части; суммирование счётчиков |
|
||||||
|
| 3 | Численное интегрирование (трапеции) | значение интеграла | миллионы вычислений f(x) | разбиение отрезка; сумма частичных площадей |
|
||||||
|
| 4 | Подсчёт простых чисел ≤ M (перебор делителей) | количество простых | O(N·√N) на чистом Python | разбиение диапазона; сумма счётчиков |
|
||||||
|
| 5 | Задача N ферзей (подсчёт расстановок) | число решений | комбинаторный перебор | ветка = первый ферзь в колонке; сумма решений |
|
||||||
|
| 6 | Частотный анализ текста (подсчёт слов) | словарь «слово → число» | построчная обработка миллионов слов | разбиение по строкам; слияние словарей |
|
||||||
|
| 7 | «Размытие» матрицы (box blur 3×3) | контрольная сумма нового изображения | 9 операций на пиксель на чистом Python | разбиение по строкам с перекрытием (halo) |
|
||||||
|
| 8 | Хеширование строк (SHA-256) | контрольная сумма дайджестов | сотни тысяч хеширований | независимые элементы; **hashlib освобождает GIL** |
|
||||||
|
| 9 | Сортировка слиянием больших массивов | отсортированный массив | сортировка миллионов элементов | сортировка кусков + merge; дорогостоящая пересылка данных |
|
||||||
|
| 10 | Игра «Жизнь» (клеточный автомат) | число живых клеток после T поколений | обход поля на каждое поколение | параллелятся поколения (шаги), обмен каймой строк |
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
- Задачи 1–5, 7, 10 — «честно» последовательные на чистом Python: потоки их
|
||||||
|
**не** ускоряют (GIL), процессы — ускоряют почти линейно.
|
||||||
|
- Задача 8 — исключение: `hashlib` освобождает GIL на время хеширования
|
||||||
|
**одного блока**, поэтому потоки дают реальное ускорение, если блоки
|
||||||
|
крупные (≥ 256 КБ). На мелких блоках ускорения нет — GIL возвращается
|
||||||
|
мгновенно. Используется в лабораторной 3 (сравнение 4 КБ против 1 МБ).
|
||||||
|
- Задача 9 — демонстрация **накладных расходов**: пересылка данных процессам
|
||||||
|
может съесть весь выигрыш от сортировки. Отличный материал для выводов в отчёте.
|
||||||
|
- Задача 10 — знакомство с идеей обмена «каймой» (halo exchange), как в
|
||||||
|
реальных сеточных методах и в MPI.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
common.benchmark — утилиты для замеров времени и построения таблиц/графиков.
|
||||||
|
|
||||||
|
Используется во всех лабораторных (1–7), чтобы отчёты были единообразными.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
from common.benchmark import time_call, make_table, save_speedup_plot
|
||||||
|
|
||||||
|
elapsed, result = time_call(lambda: task["kernel"](chunk), repeats=1)
|
||||||
|
rows = [("последовательно", T1), ("потоки, p=4", T2)]
|
||||||
|
print(make_table(rows))
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import statistics
|
||||||
|
import time
|
||||||
|
from typing import Callable, Dict, List, Sequence, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def time_call(fn: Callable, args: tuple = (), repeats: int = 3) -> Tuple[float, any]:
|
||||||
|
"""Замер времени выполнения fn(*args) с повторами.
|
||||||
|
|
||||||
|
Возвращает (лучшее_время_сек, результат). Лучшее (минимальное) время —
|
||||||
|
стандартная практика: случайные помехи только удлиняют замер.
|
||||||
|
"""
|
||||||
|
best = float("inf")
|
||||||
|
result = None
|
||||||
|
for _ in range(repeats):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = fn(*args)
|
||||||
|
best = min(best, time.perf_counter() - t0)
|
||||||
|
return best, result
|
||||||
|
|
||||||
|
|
||||||
|
def speedup(T1: float, Tp: float) -> float:
|
||||||
|
"""Ускорение S(p) = T(1) / T(p)."""
|
||||||
|
if Tp <= 0:
|
||||||
|
return 0.0
|
||||||
|
return T1 / Tp
|
||||||
|
|
||||||
|
|
||||||
|
def efficiency(speedup_val: float, p: int) -> float:
|
||||||
|
"""Эффективность E(p) = S(p) / p (доля, а не проценты)."""
|
||||||
|
if p <= 0:
|
||||||
|
return 0.0
|
||||||
|
return speedup_val / p
|
||||||
|
|
||||||
|
|
||||||
|
def amdahl(speedup_bound: float, p: int) -> float:
|
||||||
|
"""Теоретическое ускорение по закону Амдала.
|
||||||
|
|
||||||
|
speedup_bound — доля последовательной части (0..1), т.е. доля кода,
|
||||||
|
которая не распараллеливается.
|
||||||
|
"""
|
||||||
|
s = max(1e-12, speedup_bound)
|
||||||
|
return 1.0 / (s + (1.0 - s) / p)
|
||||||
|
|
||||||
|
|
||||||
|
def make_table(rows: Sequence[Tuple[str, float]], T1: float = None,
|
||||||
|
title: str = "Замеры") -> str:
|
||||||
|
"""Текстовая таблица: подход | T, c | S(p) | E(p).
|
||||||
|
|
||||||
|
rows — пары (название, время в секундах). T1 — время последовательного
|
||||||
|
запуска (первая строка, если не задано отдельно).
|
||||||
|
"""
|
||||||
|
if T1 is None:
|
||||||
|
T1 = rows[0][1]
|
||||||
|
lines = [
|
||||||
|
f"{title}",
|
||||||
|
f"{'подход':<32} {'T, c':>10} {'S(p)':>8} {'E(p)':>8}",
|
||||||
|
"-" * 62,
|
||||||
|
]
|
||||||
|
for name, t in rows:
|
||||||
|
s = speedup(T1, t)
|
||||||
|
p = _p_from_name(name)
|
||||||
|
e = efficiency(s, p)
|
||||||
|
lines.append(f"{name:<32} {t:>10.3f} {s:>8.2f} {e:>8.2f}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _p_from_name(name: str) -> int:
|
||||||
|
"""Пытается вытащить число воркеров из названия ('потоки, p=4' -> 4)."""
|
||||||
|
if "p=" in name:
|
||||||
|
try:
|
||||||
|
return int(name.split("p=")[1].split()[0].strip("),"))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def save_speedup_plot(results: Dict[str, List[Tuple[int, float]]], title: str,
|
||||||
|
out_path: str, show: bool = False):
|
||||||
|
"""График ускорения S(p) для нескольких подходов.
|
||||||
|
|
||||||
|
results = {"потоки": [(1, 1.0), (2, 1.9), (4, 3.8)],
|
||||||
|
"процессы": [(1, 1.0), (2, 1.95), (4, 3.7)]}
|
||||||
|
|
||||||
|
Для каждого подхода S(p) считается относительно его же T(1) — так
|
||||||
|
кривые корректно сравниваются между собой.
|
||||||
|
"""
|
||||||
|
import matplotlib
|
||||||
|
if not show:
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 5))
|
||||||
|
for name, pts in results.items():
|
||||||
|
pts = sorted(pts)
|
||||||
|
base = next((t for p, t in pts if p == 1), None)
|
||||||
|
if base is None:
|
||||||
|
continue
|
||||||
|
xs = [p for p, _ in pts]
|
||||||
|
ys = [base / t for _, t in pts]
|
||||||
|
plt.plot(xs, ys, marker="o", label=name)
|
||||||
|
plt.plot(xs, xs, "--", color="gray", label="идеал S(p)=p")
|
||||||
|
plt.xlabel("число воркеров p")
|
||||||
|
plt.ylabel("ускорение S(p) = T(1)/T(p)")
|
||||||
|
plt.title(title)
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True, alpha=0.3)
|
||||||
|
plt.tight_layout()
|
||||||
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||||
|
plt.savefig(out_path, dpi=150)
|
||||||
|
if show:
|
||||||
|
plt.show()
|
||||||
|
plt.close()
|
||||||
|
print(f"график сохранён: {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_bar_plot(results: Dict[str, float], title: str, out_path: str,
|
||||||
|
ylabel: str = "время, c", show: bool = False):
|
||||||
|
"""Столбчатая диаграмма: подход -> значение (время или ускорение)."""
|
||||||
|
import matplotlib
|
||||||
|
if not show:
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
names = list(results.keys())
|
||||||
|
vals = [results[k] for k in names]
|
||||||
|
plt.figure(figsize=(9, 5))
|
||||||
|
plt.bar(names, vals, color="steelblue")
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
plt.title(title)
|
||||||
|
plt.xticks(rotation=20, ha="right")
|
||||||
|
plt.grid(True, axis="y", alpha=0.3)
|
||||||
|
plt.tight_layout()
|
||||||
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||||
|
plt.savefig(out_path, dpi=150)
|
||||||
|
if show:
|
||||||
|
plt.show()
|
||||||
|
plt.close()
|
||||||
|
print(f"график сохранён: {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def median_time(fn: Callable, args: tuple = (), repeats: int = 5) -> Tuple[float, any]:
|
||||||
|
"""Аналог time_call, но по медиане — устойчив к выбросам."""
|
||||||
|
times = []
|
||||||
|
result = None
|
||||||
|
for _ in range(repeats):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
result = fn(*args)
|
||||||
|
times.append(time.perf_counter() - t0)
|
||||||
|
return statistics.median(times), result
|
||||||
+795
@@ -0,0 +1,795 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
common.tasks — ядра CPU-задач и унифицированный доступ к вариантам.
|
||||||
|
|
||||||
|
Все ядра «честно» последовательные на чистом Python (без NumPy), чтобы
|
||||||
|
эффект GIL и накладные расходы параллелизма были видны честно.
|
||||||
|
|
||||||
|
Единая схема каждой CPU-задачи (MapReduce-подобная):
|
||||||
|
|
||||||
|
data = build(params) # подготовить данные
|
||||||
|
chunks = split(data, parts) # разбить работу на части
|
||||||
|
partial = kernel(chunk) # вычислить одну часть
|
||||||
|
raw = combine(partials) # собрать итог
|
||||||
|
value = checksum(raw) # свёртка результата для таблиц/сравнений
|
||||||
|
|
||||||
|
Важно: checksum не зависит от способа разбиения — поэтому последовательный и
|
||||||
|
любой параллельный запуск обязаны давать одинаковый checksum.
|
||||||
|
|
||||||
|
Для multiprocessing функции передавать напрямую нельзя (вложенные функции не
|
||||||
|
пиккелятся). Используйте dispatch_kernel модульного уровня:
|
||||||
|
|
||||||
|
from common.tasks import dispatch_kernel
|
||||||
|
with Pool(p) as pool:
|
||||||
|
partials = pool.map(dispatch_kernel, [(name, ch) for ch in chunks])
|
||||||
|
|
||||||
|
На верхнем уровне модуля только определения — безопасно для spawn (Windows).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from collections import Counter
|
||||||
|
from typing import Any, Callable, Dict, List, Sequence, Tuple
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Реестр задач
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TASKS: Dict[str, Dict[str, Callable]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register(name: str):
|
||||||
|
def wrap(fn):
|
||||||
|
TASKS[name] = fn()
|
||||||
|
return fn
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 1. Умножение матриц (наивное, чистый Python)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("matmul")
|
||||||
|
def _task_matmul():
|
||||||
|
def build(params):
|
||||||
|
n = params["n"]
|
||||||
|
rng = random.Random(42)
|
||||||
|
A = [[rng.random() for _ in range(n)] for _ in range(n)]
|
||||||
|
B = [[rng.random() for _ in range(n)] for _ in range(n)]
|
||||||
|
return (A, B)
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
A, B = data
|
||||||
|
n = len(A)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [(A, B, bounds[i], bounds[i + 1]) for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
A, B, i0, i1 = chunk
|
||||||
|
n = len(A)
|
||||||
|
# транспонируем B для линейного доступа по памяти (эффект кэша)
|
||||||
|
Bt = [[B[j][k] for j in range(n)] for k in range(n)]
|
||||||
|
C = []
|
||||||
|
for i in range(i0, i1):
|
||||||
|
Ai = A[i]
|
||||||
|
row = []
|
||||||
|
for j in range(n):
|
||||||
|
Bj = Bt[j]
|
||||||
|
s = 0.0
|
||||||
|
for k in range(n):
|
||||||
|
s += Ai[k] * Bj[k]
|
||||||
|
row.append(s)
|
||||||
|
C.append(row)
|
||||||
|
return (i0, C)
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
C = []
|
||||||
|
for _, rows in sorted(partials, key=lambda r: r[0]):
|
||||||
|
C.extend(rows)
|
||||||
|
return C
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
s = 0.0
|
||||||
|
for row in raw:
|
||||||
|
for x in row:
|
||||||
|
s += x
|
||||||
|
return s
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"n": {"S": 140, "M": 180, "L": 220}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"умножение матриц {params['n']}x{params['n']}"
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 2. Число π методом Монте-Карло
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 2. Число π методом Монте-Карло
|
||||||
|
#
|
||||||
|
# Важный приём: total бросков делится на ФИКСИРОВАННЫЕ серии по SERIES_SIZE
|
||||||
|
# бросков, у каждой серии своё зерно по её номеру. Кусок = диапазон номеров
|
||||||
|
# серий. Поэтому checksum не зависит от того, как серии сгруппированы в
|
||||||
|
# куски — приём, реально применяемый в MC-расчётах для воспроизводимости.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("pi_mc")
|
||||||
|
def _task_pi_mc():
|
||||||
|
SERIES_SIZE = 10_000 # бросков в серии; серия = единица воспроизводимости
|
||||||
|
|
||||||
|
def build(params):
|
||||||
|
return params["total"]
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
total = data
|
||||||
|
n_series = total // SERIES_SIZE
|
||||||
|
bounds = [i * n_series // parts for i in range(parts + 1)]
|
||||||
|
return [((i, bounds[i], bounds[i + 1]), total) for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
(idx, s0, s1), _total = chunk
|
||||||
|
inside = 0
|
||||||
|
count = 0
|
||||||
|
for s in range(s0, s1):
|
||||||
|
rng = random.Random(s) # зерно = номер серии
|
||||||
|
for _ in range(SERIES_SIZE):
|
||||||
|
x = rng.random()
|
||||||
|
y = rng.random()
|
||||||
|
if x * x + y * y <= 1.0:
|
||||||
|
inside += 1
|
||||||
|
count += SERIES_SIZE
|
||||||
|
return (inside, count)
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
inside = sum(p[0] for p in partials)
|
||||||
|
total = sum(p[1] for p in partials)
|
||||||
|
return 4.0 * inside / total
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"total": {"S": 2_000_000, "M": 4_000_000, "L": 8_000_000}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"π методом Монте-Карло, {params['total']:,} бросков".replace(",", " ")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 3. Численное интегрирование (метод средних прямоугольников)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("integrate")
|
||||||
|
def _task_integrate():
|
||||||
|
def f(x):
|
||||||
|
return 4.0 / (1.0 + x * x) # на [0, 1] интеграл = π (проверяемый ответ)
|
||||||
|
|
||||||
|
def build(params):
|
||||||
|
return params
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
a, b, n = data["a"], data["b"], data["n"]
|
||||||
|
# кусок = диапазон ИНДЕКСОВ отрезков [i0, i1): kernel считает всегда
|
||||||
|
# одни и те же отрезки, checksum не зависит от разбиения
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [(data, bounds[i], bounds[i + 1]) for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
data, i0, i1 = chunk
|
||||||
|
a, b, n = data["a"], data["b"], data["n"]
|
||||||
|
h = (b - a) / n
|
||||||
|
s = 0.0
|
||||||
|
for i in range(i0, i1):
|
||||||
|
s += f(a + (i + 0.5) * h)
|
||||||
|
return s * h
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
return sum(partials)
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
n = {"S": 3_000_000, "M": 6_000_000, "L": 10_000_000}[level]
|
||||||
|
return {"a": 0.0, "b": 10.0, "n": n}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
n_str = f"{params['n']:,}".replace(",", " ")
|
||||||
|
return (f"интегрирование [{params['a']:g}, {params['b']:g}] "
|
||||||
|
f"{n_str} отрезков")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 4. Подсчёт простых чисел ≤ M (перебор делителей)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("primes")
|
||||||
|
def _task_primes():
|
||||||
|
def _is_prime(k):
|
||||||
|
if k < 2:
|
||||||
|
return False
|
||||||
|
if k % 2 == 0:
|
||||||
|
return k == 2
|
||||||
|
d = 3
|
||||||
|
while d * d <= k:
|
||||||
|
if k % d == 0:
|
||||||
|
return False
|
||||||
|
d += 2
|
||||||
|
return True
|
||||||
|
|
||||||
|
def build(params):
|
||||||
|
return params["M"]
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
M = data
|
||||||
|
lo = 2
|
||||||
|
span = (M + 1 - lo) // parts
|
||||||
|
if span == 0:
|
||||||
|
span = 1
|
||||||
|
bounds = [lo + i * span for i in range(parts)] + [M + 1]
|
||||||
|
return [(bounds[i], min(bounds[i + 1], M + 1)) for i in range(parts)
|
||||||
|
if bounds[i] < min(bounds[i + 1], M + 1)]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
lo, hi = chunk
|
||||||
|
c = 0
|
||||||
|
for k in range(lo, hi):
|
||||||
|
if _is_prime(k):
|
||||||
|
c += 1
|
||||||
|
return c
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
return sum(partials)
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"M": {"S": 1_000_000, "M": 2_000_000, "L": 4_000_000}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"подсчёт простых чисел ≤ {params['M']:,}".replace(",", " ")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 5. Задача N ферзей (подсчёт расстановок)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("nqueens")
|
||||||
|
def _task_nqueens():
|
||||||
|
def build(params):
|
||||||
|
return params["N"]
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
N = data
|
||||||
|
# одна ветка = первый ферзь в колонке col
|
||||||
|
return [(N, col) for col in range(N)]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
N, col0 = chunk
|
||||||
|
count = 0
|
||||||
|
cols = [-1] * N
|
||||||
|
|
||||||
|
def place(row):
|
||||||
|
nonlocal count
|
||||||
|
if row == N:
|
||||||
|
count += 1
|
||||||
|
return
|
||||||
|
start = col0 if row == 0 else 0
|
||||||
|
for col in range(start, N):
|
||||||
|
ok = True
|
||||||
|
for r in range(row):
|
||||||
|
c = cols[r]
|
||||||
|
if c == col or abs(c - col) == row - r:
|
||||||
|
ok = False
|
||||||
|
break
|
||||||
|
if ok:
|
||||||
|
cols[row] = col
|
||||||
|
place(row + 1)
|
||||||
|
cols[row] = -1
|
||||||
|
|
||||||
|
place(0)
|
||||||
|
return count
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
return sum(partials)
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"N": {"S": 10, "M": 11, "L": 12}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"задача {params['N']} ферзей, подсчёт расстановок"
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 6. Частотный анализ текста
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_WORD_RE = re.compile(r"[а-яёa-z0-9]+")
|
||||||
|
|
||||||
|
_SENTENCES = [
|
||||||
|
"параллельное программирование это просто",
|
||||||
|
"потоки и процессы работают по разному",
|
||||||
|
"гил ограничивает настоящую параллельность потоков",
|
||||||
|
"асинхронный код ждёт ввод вывод эффективно",
|
||||||
|
"метод монте карло считает число пи",
|
||||||
|
"распределённые системы соединяют много машин",
|
||||||
|
"синхронизация защищает общие данные",
|
||||||
|
"очередь сообщений соединяет производителя и потребителя",
|
||||||
|
"каждый воркер обрабатывает свой кусок работы",
|
||||||
|
"ускорение зависит от доли последовательного кода",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@register("wordcount")
|
||||||
|
def _task_wordcount():
|
||||||
|
def build(params):
|
||||||
|
lines = params["lines"]
|
||||||
|
rng = random.Random(12345)
|
||||||
|
words_pool = [w for s in _SENTENCES for w in s.split()]
|
||||||
|
out = []
|
||||||
|
for _ in range(lines):
|
||||||
|
k = rng.randint(5, 20)
|
||||||
|
out.append(" ".join(rng.choice(words_pool) for _ in range(k)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
lines = data
|
||||||
|
n = len(lines)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [lines[bounds[i]:bounds[i + 1]] for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
c = Counter()
|
||||||
|
for line in chunk:
|
||||||
|
for w in _WORD_RE.findall(line):
|
||||||
|
c[w] += 1
|
||||||
|
return c
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
total = Counter()
|
||||||
|
for c in partials:
|
||||||
|
total.update(c)
|
||||||
|
return total
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
# полная сумма вхождений — не зависит от разбиения
|
||||||
|
return sum(raw.values())
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"lines": {"S": 100_000, "M": 200_000, "L": 400_000}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"частотный анализ, {params['lines']:,} строк".replace(",", " ")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 7. Box blur (размытие матрицы, чистый Python, кайма 1 строка)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("blur")
|
||||||
|
def _task_blur():
|
||||||
|
def build(params):
|
||||||
|
size = params["size"]
|
||||||
|
rng = random.Random(777)
|
||||||
|
return [[rng.random() for _ in range(size)] for _ in range(size)]
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
img = data
|
||||||
|
n = len(img)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
chunks = []
|
||||||
|
for i in range(parts):
|
||||||
|
i0, i1 = bounds[i], bounds[i + 1]
|
||||||
|
if i0 >= i1:
|
||||||
|
continue
|
||||||
|
top = max(0, i0 - 1)
|
||||||
|
bot = min(n, i1 + 1)
|
||||||
|
sub = [row[:] for row in img[top:bot]] # копия с каймой (halo)
|
||||||
|
chunks.append((sub, i0 - top, i1 - top))
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
sub, lo, hi = chunk # вычисляем строки [lo, hi) внутри sub
|
||||||
|
m = len(sub[0])
|
||||||
|
out = []
|
||||||
|
for r in range(lo, hi):
|
||||||
|
new_row = []
|
||||||
|
for c in range(m):
|
||||||
|
s = 0.0
|
||||||
|
cnt = 0
|
||||||
|
for dr in (-1, 0, 1):
|
||||||
|
rr = r + dr
|
||||||
|
if 0 <= rr < len(sub):
|
||||||
|
row = sub[rr]
|
||||||
|
for dc in (-1, 0, 1):
|
||||||
|
cc = c + dc
|
||||||
|
if 0 <= cc < m:
|
||||||
|
s += row[cc]
|
||||||
|
cnt += 1
|
||||||
|
new_row.append(s / cnt)
|
||||||
|
out.append(new_row)
|
||||||
|
return (lo, out)
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
out = []
|
||||||
|
for _, rows in sorted(partials, key=lambda r: r[0]):
|
||||||
|
out.extend(rows)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
s = 0.0
|
||||||
|
for row in raw:
|
||||||
|
for x in row:
|
||||||
|
s += x
|
||||||
|
return s
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"size": {"S": 600, "M": 900, "L": 1200}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"box blur {params['size']}x{params['size']}"
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 8. Хеширование строк SHA-256
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("hashing")
|
||||||
|
def _task_hashing():
|
||||||
|
def build(params):
|
||||||
|
lines = params["lines"]
|
||||||
|
rng = random.Random(555)
|
||||||
|
pool = [w for s in _SENTENCES for w in s.split()]
|
||||||
|
out = []
|
||||||
|
for _ in range(lines):
|
||||||
|
k = rng.randint(30, 80)
|
||||||
|
out.append(" ".join(rng.choice(pool) for _ in range(k)).encode("utf-8"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
items = data
|
||||||
|
n = len(items)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [items[bounds[i]:bounds[i + 1]] for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(items):
|
||||||
|
# свёртка по каждому элементу отдельно => результат не зависит
|
||||||
|
# от границ кусков. hashlib отпускает GIL на время одного вызова:
|
||||||
|
# на коротких строках эффект незаметен, на блоках ≥ 256 КБ потоки
|
||||||
|
# реально параллелятся (демо в лабе 3)
|
||||||
|
acc = 0
|
||||||
|
for it in items:
|
||||||
|
d = hashlib.sha256(it).digest()
|
||||||
|
acc = (acc + int.from_bytes(d[:8], "big")) & 0xFFFFFFFFFFFFFFFF
|
||||||
|
return acc
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
acc = 0
|
||||||
|
for v in partials:
|
||||||
|
acc = (acc + v) & 0xFFFFFFFFFFFFFFFF
|
||||||
|
return acc
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"lines": {"S": 300_000, "M": 600_000, "L": 1_200_000}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"хеширование SHA-256, {params['lines']:,} строк".replace(",", " ")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 9. Сортировка слиянием (куски + k-way merge)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("sortbig")
|
||||||
|
def _task_sortbig():
|
||||||
|
def build(params):
|
||||||
|
n = params["n"]
|
||||||
|
rng = random.Random(999)
|
||||||
|
return [rng.random() for _ in range(n)]
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
arr = data
|
||||||
|
n = len(arr)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [arr[bounds[i]:bounds[i + 1]] for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
return sorted(chunk)
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
import heapq
|
||||||
|
return list(heapq.merge(*partials))
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
s = 0.0
|
||||||
|
prev = -1.0
|
||||||
|
for x in raw:
|
||||||
|
if x < prev:
|
||||||
|
raise ValueError("массив не отсортирован")
|
||||||
|
s += x
|
||||||
|
prev = x
|
||||||
|
return s
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
return {"n": {"S": 1_000_000, "M": 2_000_000, "L": 4_000_000}[level]}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return f"сортировка слиянием {params['n']:,} чисел".replace(",", " ")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Задача 10. Игра «Жизнь» (параллелятся строки одного поколения)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@register("life")
|
||||||
|
def _task_life():
|
||||||
|
def build(params):
|
||||||
|
size = params["size"]
|
||||||
|
rng = random.Random(31337)
|
||||||
|
grid = [[1 if rng.random() < 0.30 else 0 for _ in range(size)]
|
||||||
|
for _ in range(size)]
|
||||||
|
return (grid, params["steps"])
|
||||||
|
|
||||||
|
def split(data, parts):
|
||||||
|
grid, _steps = data
|
||||||
|
n = len(grid)
|
||||||
|
bounds = [i * n // parts for i in range(parts + 1)]
|
||||||
|
return [(grid, bounds[i], bounds[i + 1]) for i in range(parts)
|
||||||
|
if bounds[i] < bounds[i + 1]]
|
||||||
|
|
||||||
|
def kernel(chunk):
|
||||||
|
# ОДНО поколение для строк [i0, i1). Соседние строки читаются, свои — пишутся.
|
||||||
|
grid, i0, i1 = chunk
|
||||||
|
m = len(grid[0])
|
||||||
|
band = []
|
||||||
|
for r in range(i0, i1):
|
||||||
|
new_row = [0] * m
|
||||||
|
for c in range(m):
|
||||||
|
s = 0
|
||||||
|
for dr in (-1, 0, 1):
|
||||||
|
rr = r + dr
|
||||||
|
if 0 <= rr < len(grid):
|
||||||
|
row = grid[rr]
|
||||||
|
for dc in (-1, 0, 1):
|
||||||
|
if dc == 0 and dr == 0:
|
||||||
|
continue
|
||||||
|
cc = c + dc
|
||||||
|
if 0 <= cc < m:
|
||||||
|
s += row[cc]
|
||||||
|
alive = grid[r][c] == 1
|
||||||
|
if alive and (s == 2 or s == 3):
|
||||||
|
new_row[c] = 1
|
||||||
|
elif not alive and s == 3:
|
||||||
|
new_row[c] = 1
|
||||||
|
band.append(new_row)
|
||||||
|
return (i0, band)
|
||||||
|
|
||||||
|
def combine(partials):
|
||||||
|
grid = []
|
||||||
|
for _, rows in sorted(partials, key=lambda r: r[0]):
|
||||||
|
grid.extend(rows)
|
||||||
|
return grid
|
||||||
|
|
||||||
|
def checksum(raw):
|
||||||
|
return sum(sum(row) for row in raw)
|
||||||
|
|
||||||
|
def params(level):
|
||||||
|
size = {"S": 500, "M": 600, "L": 700}[level]
|
||||||
|
steps = {"S": 20, "M": 25, "L": 30}[level]
|
||||||
|
return {"size": size, "steps": steps}
|
||||||
|
|
||||||
|
def describe(params):
|
||||||
|
return (f"игра «Жизнь» {params['size']}x{params['size']}, "
|
||||||
|
f"{params['steps']} поколений")
|
||||||
|
|
||||||
|
return dict(build=build, split=split, kernel=kernel, combine=combine,
|
||||||
|
checksum=checksum, params=params, describe=describe)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# I/O-нагрузка (общая для всех вариантов, имитация сетевых запросов)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def io_fetch(item: int, delay: float = 0.05) -> Tuple[int, int]:
|
||||||
|
"""Имитация I/O-операции: блокирующая задержка + «полезная работа».
|
||||||
|
|
||||||
|
Никаких настоящих сетевых вызовов — только time.sleep (блокирующий).
|
||||||
|
Это позволяет сравнивать threading / multiprocessing / asyncio на равных.
|
||||||
|
"""
|
||||||
|
time.sleep(delay)
|
||||||
|
return (item, item * item)
|
||||||
|
|
||||||
|
|
||||||
|
def io_items(count: int) -> List[int]:
|
||||||
|
return list(range(count))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Вызов kernel из процессов (модульного уровня — пиккелится)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def dispatch_kernel(payload: Tuple[str, Any]):
|
||||||
|
"""payload = (имя_задачи, chunk) -> частичный результат.
|
||||||
|
|
||||||
|
Pool не умеет передавать вложенные функции, поэтому dispatch живёт
|
||||||
|
на уровне модуля и диспетчеризует по имени задачи.
|
||||||
|
"""
|
||||||
|
name, chunk = payload
|
||||||
|
return TASKS[name]["kernel"](chunk)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Последовательный запуск (эталон для сравнения и проверки)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def run_sequential(task_name: str, params: dict, parts: int = 1):
|
||||||
|
"""Последовательное выполнение задачи. Возвращает (checksum, секунды).
|
||||||
|
|
||||||
|
parts > 1 означает: те же куски, что пошли бы воркерам, но выполняются
|
||||||
|
в главном процессе по очереди. Используется, чтобы проверить, что
|
||||||
|
разбиение не меняет результат.
|
||||||
|
"""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
data = task["build"](params)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
if task_name == "life":
|
||||||
|
grid, steps = data
|
||||||
|
for _ in range(steps):
|
||||||
|
chunks = task["split"]((grid, steps), parts)
|
||||||
|
partials = [task["kernel"](ch) for ch in chunks]
|
||||||
|
grid = task["combine"](partials)
|
||||||
|
raw = grid
|
||||||
|
else:
|
||||||
|
chunks = task["split"](data, parts)
|
||||||
|
partials = [task["kernel"](ch) for ch in chunks]
|
||||||
|
raw = task["combine"](partials)
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
return task["checksum"](raw), elapsed
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Известные ответы (для автотестов)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_NQUEENS_KNOWN = {4: 2, 5: 10, 6: 4, 7: 40, 8: 92, 9: 352,
|
||||||
|
10: 724, 11: 2680, 12: 14200}
|
||||||
|
|
||||||
|
|
||||||
|
def known_checksum(task_name: str, params: dict):
|
||||||
|
"""Известный правильный ответ, если он есть, иначе None.
|
||||||
|
|
||||||
|
Для pi_mc и integrate ответ известен математически (π), но с ограниченной
|
||||||
|
точностью — их проверяют автотесты отдельно.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
if task_name == "nqueens":
|
||||||
|
return _NQUEENS_KNOWN.get(params["N"])
|
||||||
|
if task_name == "primes":
|
||||||
|
M = params["M"]
|
||||||
|
sieve = bytearray([1]) * (M + 1)
|
||||||
|
sieve[0:2] = b"\x00\x00"
|
||||||
|
for p in range(2, int(M ** 0.5) + 1):
|
||||||
|
if sieve[p]:
|
||||||
|
sieve[p * p:: p] = bytearray(len(sieve[p * p:: p]))
|
||||||
|
return int(sum(sieve))
|
||||||
|
if task_name == "integrate" and params["a"] == 0.0 and params["b"] == 1.0:
|
||||||
|
return math.pi
|
||||||
|
if task_name == "pi_mc":
|
||||||
|
return math.pi # приближённо, тест сравнит с допуском
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# «Дымовые» параметры (маленькие — для автотестов и самопроверки)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_SMOKE = {
|
||||||
|
"matmul": {"n": 12},
|
||||||
|
"pi_mc": {"total": 50_000}, # кратно SERIES_SIZE=10_000
|
||||||
|
"integrate": {"a": 0.0, "b": 1.0, "n": 20_000},
|
||||||
|
"primes": {"M": 2_000},
|
||||||
|
"nqueens": {"N": 6},
|
||||||
|
"wordcount": {"lines": 2_000},
|
||||||
|
"blur": {"size": 40},
|
||||||
|
"hashing": {"lines": 5_000},
|
||||||
|
"sortbig": {"n": 20_000},
|
||||||
|
"life": {"size": 20, "steps": 3},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_params(task_name: str) -> dict:
|
||||||
|
return dict(_SMOKE[task_name])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Варианты
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_VARIANTS: Dict[int, Tuple[str, str]] = {
|
||||||
|
1: ("matmul", "S"),
|
||||||
|
2: ("pi_mc", "M"),
|
||||||
|
3: ("integrate", "L"),
|
||||||
|
4: ("primes", "S"),
|
||||||
|
5: ("nqueens", "M"),
|
||||||
|
6: ("wordcount", "L"),
|
||||||
|
7: ("blur", "S"),
|
||||||
|
8: ("hashing", "M"),
|
||||||
|
9: ("sortbig", "L"),
|
||||||
|
10: ("life", "S"),
|
||||||
|
11: ("matmul", "M"),
|
||||||
|
12: ("pi_mc", "L"),
|
||||||
|
13: ("integrate", "S"),
|
||||||
|
14: ("primes", "M"),
|
||||||
|
15: ("nqueens", "L"),
|
||||||
|
16: ("wordcount", "S"),
|
||||||
|
17: ("blur", "M"),
|
||||||
|
18: ("hashing", "L"),
|
||||||
|
19: ("sortbig", "S"),
|
||||||
|
20: ("life", "M"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_IO_BY_LEVEL = {"S": (24, 0.05), "M": (48, 0.05), "L": (96, 0.05)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_variant(num: int) -> Dict[str, Any]:
|
||||||
|
"""Параметры варианта по номеру студента в журнале (1–20)."""
|
||||||
|
if not 1 <= num <= 20:
|
||||||
|
raise ValueError("номер варианта должен быть от 1 до 20")
|
||||||
|
task_name, level = _VARIANTS[num]
|
||||||
|
task = TASKS[task_name]
|
||||||
|
cpu_params = task["params"](level)
|
||||||
|
io_items_n, io_delay = _IO_BY_LEVEL[level]
|
||||||
|
return {
|
||||||
|
"variant": num,
|
||||||
|
"task_name": task_name,
|
||||||
|
"level": level,
|
||||||
|
"cpu_params": cpu_params,
|
||||||
|
"io": {"items": io_items_n, "delay": io_delay},
|
||||||
|
"p_list": [1, 2, 4] if level in ("S", "M") else [1, 2, 4, 8],
|
||||||
|
"describe": task["describe"](cpu_params),
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Варианты заданий (20 вариантов)
|
||||||
|
|
||||||
|
Вариант = номер студента в журнале (1–20). Если студентов больше 20 — вариант
|
||||||
|
`N` = `(N - 1) % 20 + 1`.
|
||||||
|
|
||||||
|
Каждый вариант состоит из:
|
||||||
|
|
||||||
|
1. **CPU-задача** (из `bank_задач.md`) с размером данных по уровню S/M/L;
|
||||||
|
2. **I/O-нагрузка** — одинаковая для всех по смыслу: `N_items` «запросов»,
|
||||||
|
каждый имитируется задержкой `delay = 0.05 c` (`common/tasks.py: io_fetch`);
|
||||||
|
3. **Список p** — количества потоков/процессов для замеров T(p).
|
||||||
|
|
||||||
|
| Вариант | Задача | Уровень | Размер данных | I/O (items × delay) | p для замеров |
|
||||||
|
|--------:|--------|:-------:|---------------|:-------------------:|:-------------:|
|
||||||
|
| 1 | 1. Умножение матриц | S | n = 140 | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 2 | 2. π методом Монте-Карло | M | 4 000 000 бросков | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 3 | 3. Интегрирование | L | 10 000 000 отрезков, [0, 10] | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 4 | 4. Простые числа | S | M = 1 000 000 | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 5 | 5. N ферзей | M | N = 11 | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 6 | 6. Частотный анализ | L | 400 000 строк | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 7 | 7. Размытие (box blur) | S | 600 × 600 | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 8 | 8. Хеширование SHA-256 | M | 600 000 строк | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 9 | 9. Сортировка слиянием | L | 4 000 000 чисел | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 10 | 10. Игра «Жизнь» | S | 500 × 500, 20 поколений | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 11 | 1. Умножение матриц | M | n = 180 | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 12 | 2. π методом Монте-Карло | L | 8 000 000 бросков | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 13 | 3. Интегрирование | S | 3 000 000 отрезков, [0, 10] | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 14 | 4. Простые числа | M | M = 2 000 000 | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 15 | 5. N ферзей | L | N = 12 | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 16 | 6. Частотный анализ | S | 100 000 строк | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 17 | 7. Размытие (box blur) | M | 900 × 900 | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 18 | 8. Хеширование SHA-256 | L | 1 200 000 строк | 96 × 0.05 c | 1, 2, 4, 8 |
|
||||||
|
| 19 | 9. Сортировка слиянием | S | 1 000 000 чисел | 24 × 0.05 c | 1, 2, 4 |
|
||||||
|
| 20 | 10. Игра «Жизнь» | M | 700 × 700, 30 поколений | 48 × 0.05 c | 1, 2, 4 |
|
||||||
|
|
||||||
|
## Уровни размеров (справочно)
|
||||||
|
|
||||||
|
| Задача | S | M | L |
|
||||||
|
|--------|---|---|---|
|
||||||
|
| 1. Матрицы | 140 | 180 | 220 |
|
||||||
|
| 2. π Монте-Карло | 2 000 000 | 4 000 000 | 8 000 000 |
|
||||||
|
| 3. Интегрирование | 3 000 000 | 6 000 000 | 10 000 000 |
|
||||||
|
| 4. Простые числа | 1 000 000 | 2 000 000 | 4 000 000 |
|
||||||
|
| 5. N ферзей | 10 | 11 | 12 |
|
||||||
|
| 6. Частотный анализ | 100 000 | 200 000 | 400 000 |
|
||||||
|
| 7. Box blur | 600 | 900 | 1200 |
|
||||||
|
| 8. Хеширование | 300 000 | 600 000 | 1 200 000 |
|
||||||
|
| 9. Сортировка | 1 000 000 | 2 000 000 | 4 000 000 |
|
||||||
|
| 10. «Жизнь» | 500×500, 20 пок. | 700×700, 30 пок. | 900×900, 40 пок. |
|
||||||
|
|
||||||
|
## Как получить свои параметры в коде
|
||||||
|
|
||||||
|
```python
|
||||||
|
from common.tasks import get_variant
|
||||||
|
|
||||||
|
v = get_variant(13) # ваш номер варианта
|
||||||
|
print(v["task_name"]) # "integrate"
|
||||||
|
print(v["cpu_params"]) # {"a": 0.0, "b": 10.0, "n": 3_000_000}
|
||||||
|
print(v["io"]) # {"items": 24, "delay": 0.05, "concurrency": 4}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Правило добросовестности
|
||||||
|
|
||||||
|
Параметры варианта менять нельзя (иначе сравнение T(p) между студентами теряет
|
||||||
|
смысл). Если ваша машина слишком медленная для уровня L — согласуйте с
|
||||||
|
преподавателем переход на уровень M и отразите это в отчёте.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Типичные ошибки и как их избежать
|
||||||
|
|
||||||
|
Сборник реальных проблем, с которыми сталкиваются студенты на этом курсе.
|
||||||
|
Прочитайте перед началом работы — сэкономит часы отладки.
|
||||||
|
|
||||||
|
## 1. `multiprocessing` на Windows: `BrokenProcessPool` / бесконечный цикл
|
||||||
|
|
||||||
|
**Симптом:** при запуске кода с `Pool` или `Process` на Windows процесс
|
||||||
|
перезапускается бесконечно или падает с ошибкой пиккелинга.
|
||||||
|
|
||||||
|
**Причина:** Windows использует метод запуска `spawn` — каждый дочерний
|
||||||
|
процесс заново импортирует ваш модуль. Если создание воркеров не защищено,
|
||||||
|
оно повторяется в каждом импорте.
|
||||||
|
|
||||||
|
**Лечение:** весь код создания процессов — только под
|
||||||
|
`if __name__ == "__main__":`. Это правило №1 курса.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
with Pool(4) as pool:
|
||||||
|
...
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. «Мой параллельный код медленнее последовательного»
|
||||||
|
|
||||||
|
**Причины по частоте:**
|
||||||
|
1. Накладные расходы (запуск процесса ~10–100 мс, передача данных через
|
||||||
|
pickle) больше, чем выгода, — работа слишком мелкая.
|
||||||
|
2. GIL: потоки на CPU-bound задаче не ускоряют, а только добавляют
|
||||||
|
переключения.
|
||||||
|
3. Слишком много частей: 10 000 задач по 1 мс — плохо; 4–8 частей по
|
||||||
|
100 мс — хорошо.
|
||||||
|
|
||||||
|
**Что делать:** это не ошибка, а результат для анализа. Опишите его в отчёте —
|
||||||
|
именно это и является целью курса.
|
||||||
|
|
||||||
|
## 3. Результат параллельного запуска не совпадает с последовательным
|
||||||
|
|
||||||
|
**Причины:**
|
||||||
|
- результат зависит от порядка сборки частичных результатов;
|
||||||
|
- несколько воркеров изменяют общую структуру без синхронизации (гонка);
|
||||||
|
- случайные данные без фиксированного seed.
|
||||||
|
|
||||||
|
**Лечение:** в этом курсе все задачи устроены так, что checksum не зависит от
|
||||||
|
разбиения. Если не совпадает — ищите гонку или неверную сборку.
|
||||||
|
|
||||||
|
## 4. Дедлок в producer–consumer
|
||||||
|
|
||||||
|
**Симптом:** программа висит и не завершается.
|
||||||
|
|
||||||
|
**Частые причины:**
|
||||||
|
- потребитель ждёт `queue.get()` в бесконечном цикле, а производитель уже
|
||||||
|
закончил — используйте `queue.put(None)` как «маркер конца» или
|
||||||
|
`queue.close()` + `join_thread()`;
|
||||||
|
- `pool.apply()` без закрытия пула;
|
||||||
|
- ожидание события, которое никто не установит.
|
||||||
|
|
||||||
|
**Отладка:** добавьте print перед/после каждого get/put — сразу видно, кто
|
||||||
|
кого не дождался.
|
||||||
|
|
||||||
|
## 5. `asyncio.run()` нельзя вложить
|
||||||
|
|
||||||
|
**Симптом:** `RuntimeError: asyncio.run() cannot be called from a running
|
||||||
|
event loop`.
|
||||||
|
|
||||||
|
**Причина:** попытка запустить event loop внутри уже работающего (например,
|
||||||
|
из Jupyter).
|
||||||
|
|
||||||
|
**Лечение:** один `asyncio.run(main())` на программу, вызывается из обычного
|
||||||
|
синхронного кода.
|
||||||
|
|
||||||
|
## 6. Забыли `await`
|
||||||
|
|
||||||
|
**Симптом:** `RuntimeWarning: coroutine ... was never awaited`, код
|
||||||
|
выполняется мгновенно и ничего не делает.
|
||||||
|
|
||||||
|
**Причина:** вызов корутины без `await` создаёт объект корутины, но не
|
||||||
|
запускает её.
|
||||||
|
|
||||||
|
**Лечение:** `await корутина()` — всегда.
|
||||||
|
|
||||||
|
## 7. Кириллица «кракозябрами» в консоли Windows
|
||||||
|
|
||||||
|
```bat
|
||||||
|
chcp 65001
|
||||||
|
```
|
||||||
|
или запуск через `python -X utf8 файл.py`. В VS Code терминал обычно уже UTF-8.
|
||||||
|
|
||||||
|
## 8. `pytest` не находит модуль студента
|
||||||
|
|
||||||
|
**Причина:** тесты запускаются не из корня курса, или solution.py лежит не там.
|
||||||
|
|
||||||
|
**Лечение:** запускать `python -m pytest tests/ -k labXX` строго из корня
|
||||||
|
курса; `solution.py` — в папке своей лабораторной.
|
||||||
|
|
||||||
|
## 9. Замеры «прыгают»
|
||||||
|
|
||||||
|
- закройте браузер/IDE-мессенджеры на время замеров;
|
||||||
|
- на ноутбуке подключите питание (троттлинг на батарее режет частоту вдвое);
|
||||||
|
- берите лучшее время из ≥3 повторов (`time_call`);
|
||||||
|
- не замеряйте в отладчике.
|
||||||
|
|
||||||
|
## 10. Списывание
|
||||||
|
|
||||||
|
Код проверяется и на совпадение между собой, и на понимание при защите.
|
||||||
|
Варианты у всех разные (см. `common/варианты.md`) — чужой вариант виден сразу.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Чек-лист сдачи лабораторной
|
||||||
|
|
||||||
|
Перед сдачей пройдитесь по списку — это снимает 90% возвратов на доработку.
|
||||||
|
|
||||||
|
## Код
|
||||||
|
|
||||||
|
- [ ] `solution.py` заполнен, интерфейсы (имена функций, сигнатуры) не менялись —
|
||||||
|
по ним работают автотесты
|
||||||
|
- [ ] В файлах, где создаются процессы (`multiprocessing`), есть
|
||||||
|
`if __name__ == "__main__":` — иначе на Windows будет `BrokenProcessPool`
|
||||||
|
- [ ] Нет абсолютных путей и путей с диском (`C:\...`) — только относительные
|
||||||
|
- [ ] Код запускается из корня курса: `python labXX_.../solution.py`
|
||||||
|
- [ ] Нет `print`-мусора; вывод осмысленный (таблицы, результаты, checksum)
|
||||||
|
- [ ] Нет «магического» ускорения за счёт уменьшения объёма работы:
|
||||||
|
checksum параллельного запуска совпадает с последовательным
|
||||||
|
|
||||||
|
## Замеры
|
||||||
|
|
||||||
|
- [ ] Замерены T(p) для всех p из вашего варианта (например 1, 2, 4)
|
||||||
|
- [ ] Каждый замер — минимум 3 повтора, берётся лучшее время
|
||||||
|
- [ ] Замеры сделаны на одной машине, без фоновых программ
|
||||||
|
- [ ] Последовательный запуск тоже замерен (это T(1), база для сравнения)
|
||||||
|
|
||||||
|
## Отчёт
|
||||||
|
|
||||||
|
- [ ] Заполнен по `common/шаблон_отчёта.md`
|
||||||
|
- [ ] Таблица замеров: T(p), S(p), E(p)
|
||||||
|
- [ ] График ускорения S(p) (картинка вставлена в отчёт)
|
||||||
|
- [ ] Анализ: почему ускорение такое, где накладные расходы, что ограничивает
|
||||||
|
- [ ] Ответы на контрольные вопросы из методички
|
||||||
|
- [ ] Указаны ОС, CPU и параметры варианта
|
||||||
|
|
||||||
|
## Самопроверка
|
||||||
|
|
||||||
|
- [ ] `python -m pytest tests/ -k labXX` — все тесты зелёные
|
||||||
|
- [ ] Код отформатирован, нет закомментированных кусков и TODO-заглушек
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Шаблон отчёта по лабораторной работе
|
||||||
|
|
||||||
|
> Скопируйте этот файл в папку своей лабораторной (например,
|
||||||
|
> `lab02_threads/отчёт.md`), заполните и приложите графики.
|
||||||
|
|
||||||
|
- **ФИО:** ____________
|
||||||
|
- **Группа:** ____________
|
||||||
|
- **Вариант:** № ____ из `common/варианты.md`
|
||||||
|
- **Задача:** ____ (описание + параметры варианта)
|
||||||
|
- **ОС / CPU / объём RAM:** ____ (например: Windows 11, AMD Ryzen 5 5600, 16 ГБ)
|
||||||
|
|
||||||
|
## 1. Цель работы
|
||||||
|
|
||||||
|
(2–3 предложения своими словами)
|
||||||
|
|
||||||
|
## 2. Краткая теория
|
||||||
|
|
||||||
|
(5–10 предложений: какие механизмы использовались и почему именно они
|
||||||
|
подходят/не подходят для вашей задачи. Переписывать методичку не нужно —
|
||||||
|
сформулируйте понимание своими словами.)
|
||||||
|
|
||||||
|
## 3. Описание решения
|
||||||
|
|
||||||
|
- что разбивается на части и почему именно так;
|
||||||
|
- что выполняет каждый воркер;
|
||||||
|
- как собирается итог;
|
||||||
|
- какие средства синхронизации использованы и зачем (если использовались).
|
||||||
|
|
||||||
|
## 4. Результаты замеров
|
||||||
|
|
||||||
|
Таблица: время T(p) для p = 1, 2, 4 (…), ускорение S(p) = T(1)/T(p),
|
||||||
|
эффективность E(p) = S(p)/p.
|
||||||
|
|
||||||
|
| Подход | T, c | S(p) | E(p) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| последовательно | | 1.00 | 1.00 |
|
||||||
|
| потоки, p=2 | | | |
|
||||||
|
| потоки, p=4 | | | |
|
||||||
|
|
||||||
|
Как замерять: `common/benchmark.py` (`time_call`, минимум 3 повтора,
|
||||||
|
берётся лучшее время). Замеры — на спокойной машине, без фоновых программ.
|
||||||
|
|
||||||
|
## 5. Графики
|
||||||
|
|
||||||
|
- график ускорения S(p) — `save_speedup_plot` из `common/benchmark.py`;
|
||||||
|
- при необходимости — столбчатая диаграмма времени.
|
||||||
|
|
||||||
|
Вставьте сюда картинки:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 6. Анализ
|
||||||
|
|
||||||
|
Ответьте письменно (по 2��5 предложений на пункт):
|
||||||
|
|
||||||
|
1. Получено ли ускорение? На сколько? Совпадает ли с ожиданиями?
|
||||||
|
2. Почему именно такая модель (потоки/процессы/async) подходит или не
|
||||||
|
подходит вашей задаче?
|
||||||
|
3. Наблюдались ли накладные расходы (запуск процессов, передача данных,
|
||||||
|
синхронизация)? Где они видны в замерах?
|
||||||
|
4. Что ограничивает дальнейший рост ускорения (GIL, память, число ядер,
|
||||||
|
доля последовательного кода)?
|
||||||
|
|
||||||
|
## 7. Выводы
|
||||||
|
|
||||||
|
(3–6 предложений: что изучено, какие практические выводы сделаны.)
|
||||||
|
|
||||||
|
## 8. Ответы на контрольные вопросы
|
||||||
|
|
||||||
|
(Из конца методички. Полными предложениями, своими словами.)
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная 0: проверка готовности окружения.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab00_intro/check_env.py
|
||||||
|
|
||||||
|
Скрипт проверяет версию Python, библиотеки, число ядер и работоспособность
|
||||||
|
threading / multiprocessing / asyncio. Все проверки должны напечатать [OK].
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# делаем доступным импорт common.* при запуске из корня курса
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
FAILED = []
|
||||||
|
|
||||||
|
|
||||||
|
def check(name: str, fn):
|
||||||
|
try:
|
||||||
|
detail = fn()
|
||||||
|
print(f"[OK] {name}{(': ' + str(detail)) if detail else ''}")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
FAILED.append(name)
|
||||||
|
print(f"[FAIL] {name}: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_python_version():
|
||||||
|
v = sys.version_info
|
||||||
|
assert v >= (3, 10), f"нужен Python 3.10+, у вас {v.major}.{v.minor}"
|
||||||
|
return f"Python {v.major}.{v.minor}.{v.micro}"
|
||||||
|
|
||||||
|
|
||||||
|
def check_modules():
|
||||||
|
import numpy
|
||||||
|
import matplotlib
|
||||||
|
import pytest
|
||||||
|
return (f"numpy {numpy.__version__}, matplotlib {matplotlib.__version__}, "
|
||||||
|
f"pytest {pytest.__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_cpu_count():
|
||||||
|
import multiprocessing
|
||||||
|
n = multiprocessing.cpu_count()
|
||||||
|
assert n >= 2, "нужно минимум 2 логических ядра"
|
||||||
|
return f"{n} логических ядер"
|
||||||
|
|
||||||
|
|
||||||
|
def check_threading():
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
results = []
|
||||||
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||||
|
results = list(pool.map(lambda x: x * x, range(8)))
|
||||||
|
assert results == [i * i for i in range(8)]
|
||||||
|
return "ThreadPoolExecutor работает"
|
||||||
|
|
||||||
|
|
||||||
|
def _double(x):
|
||||||
|
"""Функция модульного уровня — Pool требует пиккелируемость."""
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
|
||||||
|
def check_multiprocessing():
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
ctx = multiprocessing.get_context() # spawn на Windows, fork на macOS/Linux
|
||||||
|
with ctx.Pool(2) as pool:
|
||||||
|
res = pool.map(_double, [1, 2, 3, 4])
|
||||||
|
assert res == [2, 4, 6, 8], res
|
||||||
|
return f"метод запуска: {ctx.get_start_method()}"
|
||||||
|
|
||||||
|
|
||||||
|
def check_asyncio():
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
return 42
|
||||||
|
|
||||||
|
assert asyncio.run(main()) == 42
|
||||||
|
return "event loop работает"
|
||||||
|
|
||||||
|
|
||||||
|
def check_common_tasks():
|
||||||
|
from common.tasks import get_variant
|
||||||
|
|
||||||
|
v = get_variant(1)
|
||||||
|
assert "task_name" in v and "io" in v
|
||||||
|
return f"вариант 1: {v['describe']}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("Проверка окружения для курса «Технология параллельного программирования»")
|
||||||
|
print("=" * 60)
|
||||||
|
check("Версия Python", check_python_version)
|
||||||
|
check("Библиотеки", check_modules)
|
||||||
|
check("Число ядер CPU", check_cpu_count)
|
||||||
|
check("Потоки (threading)", check_threading)
|
||||||
|
check("Процессы (multiprocessing)", check_multiprocessing)
|
||||||
|
check("Асинхронность (asyncio)", check_asyncio)
|
||||||
|
check("Банк задач (common.tasks)", check_common_tasks)
|
||||||
|
print("-" * 60)
|
||||||
|
if FAILED:
|
||||||
|
print(f"ПРОБЛЕМЫ: {', '.join(FAILED)}")
|
||||||
|
print("Разберитесь по методичке lab00_intro/методичка.md — без этого")
|
||||||
|
print("следующие лабораторные не будут работать.")
|
||||||
|
sys.exit(1)
|
||||||
|
print("Всё готово. Запомните свой вариант из common/варианты.md.")
|
||||||
|
if os.name == "nt":
|
||||||
|
print("(Кириллица отображается кракозябрами? Выполните: chcp 65001)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Лабораторная работа 0. Подготовка окружения
|
||||||
|
|
||||||
|
**Время на работу:** 1 пара. **Оценка:** зачёт/не зачёт.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Подготовить рабочее окружение и убедиться, что все механизмы параллельного
|
||||||
|
программирования доступны на вашей машине — до того, как вы начнёте основную
|
||||||
|
лабораторную.
|
||||||
|
|
||||||
|
## Теория (минимум)
|
||||||
|
|
||||||
|
### Виртуальное окружение (venv)
|
||||||
|
|
||||||
|
Виртуальное окружение — изолированная папка с собственной копией Python и
|
||||||
|
своим набором библиотек. Оно нужно, чтобы зависимости курса не конфликтовали
|
||||||
|
с другими проектами. Активированное окружение видно в терминале по префиксу
|
||||||
|
`(.venv)`.
|
||||||
|
|
||||||
|
### Почему не Google Colab / онлайн-интерпретаторы
|
||||||
|
|
||||||
|
Параллельность — это про реальные ядра CPU и реальную ОС. В облаке вы не
|
||||||
|
получите ни честного числа ядер, ни контроля над фоновыми процессами, ни
|
||||||
|
Windows-специфики `spawn`. Работаем локально.
|
||||||
|
|
||||||
|
### Модель памяти Python: всё передаётся по ссылке
|
||||||
|
|
||||||
|
В Python переменные — ссылки на объекты. Для потоков это значит: **все потоки
|
||||||
|
одного процесса видят одни и те же объекты** — отсюда гонки данных (лаба 5).
|
||||||
|
Для процессов это значит: **каждый процесс получает свою копию данных**
|
||||||
|
(через pickle) — отсюда накладные расходы (лаба 4).
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
### Шаг 1. Установите Python 3.10+
|
||||||
|
|
||||||
|
Windows: python.org → Downloads → «Windows installer (64-bit)».
|
||||||
|
**Обязательно** отметьте «Add python.exe to PATH» на первом экране установщика.
|
||||||
|
|
||||||
|
Проверка (новое окно терминала):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
python --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2. VS Code
|
||||||
|
|
||||||
|
Установите VS Code и расширение **Python** (издатель Microsoft). Откройте
|
||||||
|
папку курса: File → Open Folder. Терминал: Terminal → New Terminal
|
||||||
|
(или `` Ctrl+` ``).
|
||||||
|
|
||||||
|
### Шаг 3. Виртуальное окружение и зависимости
|
||||||
|
|
||||||
|
В терминале VS Code **из корня курса**:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS/Linux: `source .venv/bin/activate`.
|
||||||
|
|
||||||
|
Если VS Code спросит интерпретатор — выберите `.\.venv\Scripts\python.exe`.
|
||||||
|
|
||||||
|
### Шаг 4. Git (кратко, если ещё не знакомы)
|
||||||
|
|
||||||
|
```bat
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "lab 0: окружение готово"
|
||||||
|
```
|
||||||
|
|
||||||
|
Дальше фиксируйте прогресс коммитами — это история вашей работы (и аргумент
|
||||||
|
в спорах о списывании).
|
||||||
|
|
||||||
|
### Шаг 5. Проверка окружения
|
||||||
|
|
||||||
|
```bat
|
||||||
|
python lab00_intro\check_env.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт проверит версию Python, наличие библиотек, число ядер CPU и работоспособность
|
||||||
|
потоков, процессов и asyncio. Все проверки должны быть `[OK]`.
|
||||||
|
|
||||||
|
### Шаг 6. Свой вариант
|
||||||
|
|
||||||
|
```python
|
||||||
|
from common.tasks import get_variant
|
||||||
|
print(get_variant(<ваш номер в журнале>))
|
||||||
|
```
|
||||||
|
|
||||||
|
Запишите параметры — они понадобятся во всех следующих лабораторных.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Зачем нужно виртуальное окружение? Что произойдёт, если его не создавать?
|
||||||
|
2. Чем процесс отличается от потока? (Подсказка: общая память.)
|
||||||
|
3. Почему для этого курса важна работа на локальной машине, а не в облаке?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
- Скриншот вывода `check_env.py` (все проверки `[OK]`);
|
||||||
|
- короткий ответ на контрольные вопросы (можно в чате при сдаче).
|
||||||
|
|
||||||
|
## Критерии зачёта
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| Окружение развёрнуто, check_env.py всё `[OK]` | 1 |
|
||||||
|
| Ответы на контрольные вопросы | 1 |
|
||||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: perf_counter против time.time и process_time.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab01_sequential/examples/01_perf_counter_demo.py
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
- почему time.time() для замеров плох (не монотонные часы);
|
||||||
|
- как perf_counter и process_time диагностируют I/O-bound задачу.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
|
||||||
|
def demo_time_vs_perf():
|
||||||
|
print("--- time.time() против time.perf_counter() ---")
|
||||||
|
t1 = time.time()
|
||||||
|
p1 = time.perf_counter()
|
||||||
|
time.sleep(0.05)
|
||||||
|
t2 = time.time()
|
||||||
|
p2 = time.perf_counter()
|
||||||
|
print(f"time.time(): {(t2 - t1) * 1000:8.2f} мс (точность зависит от ОС)")
|
||||||
|
print(f"time.perf_counter():{(p2 - p1) * 1000:8.2f} мс (монотонные, наносекундное разрешение)")
|
||||||
|
|
||||||
|
|
||||||
|
def demo_cpu_vs_io():
|
||||||
|
print("\n--- CPU-bound против I/O-bound: perf_counter vs process_time ---")
|
||||||
|
|
||||||
|
# CPU-bound: считаем
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
c0 = time.process_time()
|
||||||
|
s = 0.0
|
||||||
|
for i in range(3_000_000):
|
||||||
|
s += i ** 0.5
|
||||||
|
cpu_wall = time.perf_counter() - t0
|
||||||
|
cpu_proc = time.process_time() - c0
|
||||||
|
|
||||||
|
# I/O-bound: ждём (имитация сетевых запросов)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
c0 = time.process_time()
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(0.05) # «ожидание ответа сервера»
|
||||||
|
io_wall = time.perf_counter() - t0
|
||||||
|
io_proc = time.process_time() - c0
|
||||||
|
|
||||||
|
print(f"{'':14}{'настенное, c':>14}{'процессорное, c':>18}{'доля сна':>10}")
|
||||||
|
print(f"{'CPU-bound':14}{cpu_wall:>14.3f}{cpu_proc:>18.3f}"
|
||||||
|
f"{100 * (1 - cpu_proc / cpu_wall):>9.0f}%")
|
||||||
|
print(f"{'I/O-bound':14}{io_wall:>14.3f}{io_proc:>18.3f}"
|
||||||
|
f"{100 * (1 - io_proc / io_wall):>9.0f}%")
|
||||||
|
print("\nВывод: у I/O-задачи процессор почти всё время спит —")
|
||||||
|
print("именно поэтому потоки/asyncio могут её ускорить (лабы 2 и 6).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo_time_vs_perf()
|
||||||
|
demo_cpu_vs_io()
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: разбиение работы на куски не должно менять результат.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab01_sequential/examples/02_split_check_demo.py
|
||||||
|
|
||||||
|
Ключевая идея курса: задача формулируется как split -> kernel -> combine ->
|
||||||
|
checksum. Разбиение — это подготовка к параллелизму: последовательный запуск
|
||||||
|
кусков обязан давать тот же checksum, что и один большой кусок.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
from common.tasks import TASKS, smoke_params
|
||||||
|
|
||||||
|
|
||||||
|
def run(task_name: str, parts: int):
|
||||||
|
"""Последовательный запуск задачи из `parts` кусков."""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
params = smoke_params(task_name)
|
||||||
|
data = task["build"](params)
|
||||||
|
chunks = task["split"](data, parts)
|
||||||
|
partials = [task["kernel"](ch) for ch in chunks]
|
||||||
|
raw = task["combine"](partials)
|
||||||
|
return task["checksum"](raw)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"{'задача':<12} {'parts=1':>18} {'parts=2':>18} {'parts=4':>18} совпадает")
|
||||||
|
for name in ("pi_mc", "integrate", "nqueens", "blur", "hashing"):
|
||||||
|
v1 = run(name, 1)
|
||||||
|
v2 = run(name, 2)
|
||||||
|
v4 = run(name, 4)
|
||||||
|
# float-результаты (π, интеграл) сравниваем с допуском: при разбиении
|
||||||
|
# меняется порядок суммирования, меняются последние биты мантиссы
|
||||||
|
def same(a, b):
|
||||||
|
if isinstance(a, float):
|
||||||
|
return abs(a - b) <= 1e-6 * max(1.0, abs(a))
|
||||||
|
return a == b
|
||||||
|
same12 = same(v1, v2)
|
||||||
|
same14 = same(v1, v4)
|
||||||
|
marker = "True" if (same12 and same14) else "False <-- ОШИБКА"
|
||||||
|
print(f"{name:<12} {v1:>18.6f} {v2:>18.6f} {v4:>18.6f} {marker}")
|
||||||
|
print("\nЕсли checksum зависит от разбиения — параллельная версия будет")
|
||||||
|
print("давать «случайный» результат. Это ошибка, а не особенность.")
|
||||||
|
print("(Float-задачи сравниваются с допуском — см. код демо.)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: cProfile — где программа проводит время.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab01_sequential/examples/03_cprofile_demo.py
|
||||||
|
|
||||||
|
На глаз две реализации выглядят одинаково. Профилировщик показывает,
|
||||||
|
что наивная transpose-версия медленнее — из-за плохой локальности памяти.
|
||||||
|
"""
|
||||||
|
import cProfile
|
||||||
|
import os
|
||||||
|
import pstats
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
N = 250
|
||||||
|
|
||||||
|
|
||||||
|
def matmul_rows(A, B):
|
||||||
|
"""Доступ к B по строкам: b[k] читается целиком (хорошо для кэша)."""
|
||||||
|
C = []
|
||||||
|
for i in range(N):
|
||||||
|
Ai, row = A[i], []
|
||||||
|
for j in range(N):
|
||||||
|
s = 0.0
|
||||||
|
for k in range(N):
|
||||||
|
s += Ai[k] * B[k][j] # B[k] — колонка, каждый раз новая строка
|
||||||
|
row.append(s)
|
||||||
|
C.append(row)
|
||||||
|
return C
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rng_a = [[float(i + j) for j in range(N)] for i in range(N)]
|
||||||
|
rng_b = [[float(i - j) for j in range(N)] for i in range(N)]
|
||||||
|
|
||||||
|
profiler = cProfile.Profile()
|
||||||
|
profiler.enable()
|
||||||
|
C = matmul_rows(rng_a, rng_b)
|
||||||
|
profiler.disable()
|
||||||
|
|
||||||
|
stream = io.StringIO()
|
||||||
|
stats = pstats.Stats(profiler, stream=stream)
|
||||||
|
stats.sort_stats("cumulative").print_stats(8)
|
||||||
|
print(stream.getvalue())
|
||||||
|
print(f"checksum = {sum(sum(r) for r in C):.3f}")
|
||||||
|
print("Смотрим tototime: почти всё время — во внутреннем цикле matmul_rows.")
|
||||||
|
print("Это и есть узкое место, которое будут распараллеливать лабы 3–4.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 1: последовательное вычисление и профилирование.
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab01.py).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab01_sequential/solution.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import make_table, time_call
|
||||||
|
from common.tasks import TASKS, get_variant, io_fetch
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
def run_cpu_sequential(task_name: str, params: dict, parts: int = 4):
|
||||||
|
"""Последовательное выполнение задачи из `parts` кусков.
|
||||||
|
|
||||||
|
Возвращает (checksum, elapsed_seconds).
|
||||||
|
|
||||||
|
Схема:
|
||||||
|
build -> split -> kernel (по очереди!) -> combine -> checksum
|
||||||
|
"""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
# TODO: реализуйте по схеме выше и замерьте время perf_counter-ом
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def run_io_sequential(items: int, delay: float = 0.05):
|
||||||
|
"""Последовательное выполнение I/O-нагрузки.
|
||||||
|
|
||||||
|
Для каждого item из range(items) вызывает io_fetch(item, delay).
|
||||||
|
Возвращает (список_результатов, elapsed_seconds).
|
||||||
|
Результаты — в порядке возрастания item.
|
||||||
|
"""
|
||||||
|
# TODO: цикл по items, сбор результатов, замер времени
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||||
|
print(f"I/O-нагрузка: {v['io']['items']} запросов по {v['io']['delay']} c\n")
|
||||||
|
|
||||||
|
# --- CPU-часть: T(1), T(2), T(4) --------------------------------------
|
||||||
|
rows, checksums = [], []
|
||||||
|
for parts in (1, 2, 4):
|
||||||
|
t, (cs, _) = time_call(run_cpu_sequential,
|
||||||
|
(v["task_name"], v["cpu_params"], parts))
|
||||||
|
rows.append((f"CPU, {parts} кусков", t))
|
||||||
|
checksums.append(cs)
|
||||||
|
print(make_table(rows, title="CPU-часть (последовательно)"))
|
||||||
|
|
||||||
|
def same(a, b):
|
||||||
|
# float-результаты сравниваем с допуском (порядок суммирования!)
|
||||||
|
if isinstance(a, float) or isinstance(b, float):
|
||||||
|
return abs(a - b) <= 1e-6 * max(1.0, abs(a))
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
ok = all(same(checksums[0], c) for c in checksums[1:])
|
||||||
|
print(f"checksum совпадает при разном разбиении: {ok}\n")
|
||||||
|
|
||||||
|
# --- I/O-часть ----------------------------------------------------------
|
||||||
|
results, t_io = run_io_sequential(v["io"]["items"], v["io"]["delay"])
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
c0 = time.process_time()
|
||||||
|
run_io_sequential(v["io"]["items"], v["io"]["delay"])
|
||||||
|
wall = time.perf_counter() - t0
|
||||||
|
proc = time.process_time() - c0
|
||||||
|
print(f"I/O-часть: {len(results)} запросов за {t_io:.3f} c")
|
||||||
|
print(f" настенное время (perf_counter): {wall:.3f} c")
|
||||||
|
print(f" процессорное время (process_time): {proc:.3f} c")
|
||||||
|
print(f" доля ожидания: {100 * (1 - proc / wall):.0f}%")
|
||||||
|
print(f" потенциал ускорения потоками (лаба 2): x{t_io / v['io']['delay']:.1f}\n")
|
||||||
|
|
||||||
|
# --- Профилирование ------------------------------------------------------
|
||||||
|
import cProfile
|
||||||
|
import io as _io
|
||||||
|
import pstats
|
||||||
|
|
||||||
|
print("--- cProfile CPU-части (топ-5 по времени) ---")
|
||||||
|
profiler = cProfile.Profile()
|
||||||
|
profiler.enable()
|
||||||
|
run_cpu_sequential(v["task_name"], v["cpu_params"], parts=4)
|
||||||
|
profiler.disable()
|
||||||
|
stream = _io.StringIO()
|
||||||
|
pstats.Stats(profiler, stream=stream).sort_stats("cumulative").print_stats(5)
|
||||||
|
print(stream.getvalue())
|
||||||
|
# TODO: выпишите в отчёт, какая функция — узкое место
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Лабораторная работа 1. Последовательное вычисление и профилирование
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Создать базовую линию: последовательную реализацию своей задачи и её замеры.
|
||||||
|
Без неё невозможно показать ускорение в следующих лабораторных — нечего будет
|
||||||
|
ускорять.
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Последовательное вычисление
|
||||||
|
|
||||||
|
Программа выполняет операцию за операцией, в одном потоке управления. Это
|
||||||
|
простейшая и самая предсказуемая модель: результат не зависит от порядка
|
||||||
|
выполнения, время выполнения = сумма времён всех операций. Все дальнейшие
|
||||||
|
лабораторные сравниваются с этой базой.
|
||||||
|
|
||||||
|
### Два типа нагрузки
|
||||||
|
|
||||||
|
**CPU-bound** — программа ограничена скоростью процессора: считает, шифрует,
|
||||||
|
сортирует. Время определяется числом элементарных операций.
|
||||||
|
|
||||||
|
**I/O-bound** — программа ограничена ожиданием: сеть, диск, база данных.
|
||||||
|
Время определяется временем ожидания ответов, процессор при этом простаивает.
|
||||||
|
|
||||||
|
Отличить просто: если удалить «полезную работу», а программа всё равно
|
||||||
|
медленная — это I/O-bound; если быстрая — CPU-bound. Тип нагрузки определяет,
|
||||||
|
какая модель параллелизма поможет (потоки — для I/O, процессы — для CPU,
|
||||||
|
async — для I/O в больших количествах).
|
||||||
|
|
||||||
|
### Время: настенное и процессорное
|
||||||
|
|
||||||
|
- `time.perf_counter()` — «настенные» часы: сколько времени прошло реально
|
||||||
|
(включая сон и ожидание). Ими меряют всё в этом курсе.
|
||||||
|
- `time.process_time()` — только время, потраченное процессором.
|
||||||
|
Разница между ними для I/O-задачи огромна — это её диагноз.
|
||||||
|
|
||||||
|
Обе функции монотонные (не откатываются назад, как `time.time()` при
|
||||||
|
переводе часов), имеют наносекундное разрешение — всегда используйте их, а не
|
||||||
|
`time.time()`.
|
||||||
|
|
||||||
|
### Профилирование
|
||||||
|
|
||||||
|
Профилировщик отвечает на вопрос «где программа проводит время». `cProfile`
|
||||||
|
собирает статистику по функциям: число вызовов, суммарное время, время без
|
||||||
|
вложенных вызовов.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import cProfile
|
||||||
|
cProfile.run("main()", sort="cumulative")
|
||||||
|
```
|
||||||
|
|
||||||
|
Интерпретация: колонка `cumtime` — общее время функции (с вызовами),
|
||||||
|
`totime` — собственное. Первые строки обычно сразу показывают узкое место.
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
Ваша задача и параметры — по варианту (`common/варианты.md`), доступ:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from common.tasks import get_variant, TASKS
|
||||||
|
v = get_variant(13) # ваш номер
|
||||||
|
task = TASKS[v["task_name"]]
|
||||||
|
data = task["build"](v["cpu_params"])
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполните `solution.py` (интерфейсы менять нельзя):
|
||||||
|
|
||||||
|
1. **`run_cpu_sequential(task_name, params, parts=4)`** — выполняет задачу
|
||||||
|
последовательно: `build` → разбиение на `parts` кусков → `kernel` для
|
||||||
|
каждого куска в цикле → `combine` → вернуть `checksum`. Куски
|
||||||
|
обязательны: в следующих лабораторных те же куски уйдут воркерам.
|
||||||
|
2. **`run_io_sequential(items, delay)`** — для каждого элемента вызывает
|
||||||
|
`common.tasks.io_fetch(item, delay)` и собирает результаты в список
|
||||||
|
(в порядке возрастания item).
|
||||||
|
3. **`main()`** — запускает обе части на параметрах своего варианта, выводит
|
||||||
|
таблицу времен (`common.benchmark.make_table`) и значения checksum.
|
||||||
|
|
||||||
|
### Измерения для отчёта
|
||||||
|
|
||||||
|
- CPU-часть: T(1) при разбиении на 1, 2, 4 куска (убедитесь, что checksum
|
||||||
|
одинаков и время почти не меняется — разбиение не должно менять работу).
|
||||||
|
- I/O-часть: суммарное время для items из варианта; сравните
|
||||||
|
`perf_counter` и `process_time` — увидите, сколько времени машина спит.
|
||||||
|
- Профилирование CPU-части: `cProfile.run(...)`, топ-5 функций по времени.
|
||||||
|
Укажите в отчёте, какая функция — узкое место.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Чем CPU-bound задача отличается от I/O-bound? Какая у вас?
|
||||||
|
2. Почему для замеров нужен `perf_counter`, а не `time.time()`?
|
||||||
|
3. Что показывает `cumtime` и `tototime` в выводе cProfile?
|
||||||
|
4. Почему checksum не должен зависеть от числа кусков? Что это проверяет?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт по шаблону: таблицы T(1/2/4 кусков), время I/O-части,
|
||||||
|
сравнение perf_counter/process_time, топ-5 из cProfile, анализ.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| `run_cpu_sequential` работает, checksum совпадает при разном разбиении | 2 |
|
||||||
|
| `run_io_sequential` работает | 1 |
|
||||||
|
| Замеры и сравнение perf_counter/process_time | 2 |
|
||||||
|
| Профилирование и вывод об узком месте | 2 |
|
||||||
|
| Отчёт: таблицы, анализ, ответы на вопросы | 3 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
См. `common/типичные_ошибки.md` (пп. 8, 9). Здесь их всего две: замеры в
|
||||||
|
отладчике и замеры без повторов.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: потоки выполняются конкурентно — sleep не складывается.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab02_threads/examples/01_threads_sleep_demo.py
|
||||||
|
|
||||||
|
Пять потоков спят по 0.4 c. Сумма времён = 2 c, реальное время ≈ 0.4 c:
|
||||||
|
пока один поток спит, процессор обслуживает другие.
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
N_THREADS = 5
|
||||||
|
SLEEP = 0.4
|
||||||
|
|
||||||
|
|
||||||
|
def worker(idx: int) -> None:
|
||||||
|
name = threading.current_thread().name
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
time.sleep(SLEEP) # «ожидание сети/диска»
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
print(f" {name}: {dt:.3f} c")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Запускаем {N_THREADS} потоков, каждый спит {SLEEP} c")
|
||||||
|
|
||||||
|
threads = [threading.Thread(target=worker, args=(i,), name=f"поток-{i}")
|
||||||
|
for i in range(N_THREADS)]
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for t in threads:
|
||||||
|
t.start() # ВСЕ стартуют первыми...
|
||||||
|
for t in threads:
|
||||||
|
t.join() # ...потом ВСЕ ждём (start и join не перемешивать!)
|
||||||
|
total = time.perf_counter() - t0
|
||||||
|
|
||||||
|
print(f"Суммарно: {total:.3f} c "
|
||||||
|
f"(сумма времён была бы {N_THREADS * SLEEP:.1f} c)")
|
||||||
|
print(f"Ускорение: x{N_THREADS * SLEEP / total:.1f} — так работает I/O-параллелизм")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: ThreadPoolExecutor, Future и порядок завершения.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab02_threads/examples/02_executor_future_demo.py
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
- pool.map: результаты в порядке входа, даже если завершились в другом;
|
||||||
|
- submit + as_completed: обрабатываем по мере готовности.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
from common.tasks import io_fetch
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# «Сервер» отвечает тем медленнее, чем больше номер запроса
|
||||||
|
delays = [0.30, 0.25, 0.20, 0.15, 0.10]
|
||||||
|
|
||||||
|
print("--- pool.map: результаты в порядке входа ---")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||||
|
results = list(pool.map(io_fetch, range(5), delays))
|
||||||
|
print(f" порядок: {[r[0] for r in results]} (как на входе)")
|
||||||
|
print(f" время: {time.perf_counter() - t0:.3f} c "
|
||||||
|
f"(последовательный был бы {sum(delays):.2f} c)\n")
|
||||||
|
|
||||||
|
print("--- submit + as_completed: по мере готовности ---")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||||
|
futures = {pool.submit(io_fetch, i, d): i for i, d in enumerate(delays)}
|
||||||
|
for fut in as_completed(futures):
|
||||||
|
item = futures[fut]
|
||||||
|
print(f" завершён запрос {item} "
|
||||||
|
f"(через {time.perf_counter() - t0:.3f} c)")
|
||||||
|
print(f" время: {time.perf_counter() - t0:.3f} c\n")
|
||||||
|
|
||||||
|
print("--- Future: .result() блокирует до готовности ---")
|
||||||
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||||
|
fut = pool.submit(io_fetch, 42, 0.20)
|
||||||
|
print(f" done() сразу после submit: {fut.done()}")
|
||||||
|
result = fut.result() # ждём
|
||||||
|
print(f" done() после result(): {fut.done()}, результат: {result}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: предвестник лабы 3 — потоки НЕ ускоряют счёт.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab02_threads/examples/03_threads_cpu_demo.py
|
||||||
|
|
||||||
|
Та же схема, что с sleep, но вместо sleep — счёт. Время не уменьшается:
|
||||||
|
GIL разрешает исполнять байт-код Python только одному потоку.
|
||||||
|
Подробное исследование — лабораторная 3.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_worker(n: int) -> None:
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
s += i ** 0.5 # чистый Python-код: держит GIL
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def run_threads(n_threads: int, n_per_thread: int) -> float:
|
||||||
|
threads = [threading.Thread(target=cpu_worker, args=(n_per_thread,))
|
||||||
|
for _ in range(n_threads)]
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
return time.perf_counter() - t0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
TOTAL = 3_000_000
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
cpu_worker(TOTAL)
|
||||||
|
t_seq = time.perf_counter() - t0
|
||||||
|
print(f"1 поток, {TOTAL:,} итераций: {t_seq:.3f} c")
|
||||||
|
|
||||||
|
t2 = run_threads(2, TOTAL // 2)
|
||||||
|
print(f"2 потока, по {TOTAL // 2:,}: {t2:.3f} c "
|
||||||
|
f"(ускорение x{t_seq / t2:.2f})")
|
||||||
|
|
||||||
|
t4 = run_threads(4, TOTAL // 4)
|
||||||
|
print(f"4 потока, по {TOTAL // 4:,}: {t4:.3f} c "
|
||||||
|
f"(ускорение x{t_seq / t4:.2f})")
|
||||||
|
|
||||||
|
print("\nУскорения нет (даже хуже!) — GIL. Но тот же код с time.sleep")
|
||||||
|
print("ускорился бы почти идеально (демо 01). Разницу изучаем в лабе 3.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 2: многопоточный код (I/O-bound).
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab02.py).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab02_threads/solution.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import make_table, save_speedup_plot, time_call
|
||||||
|
from common.tasks import get_variant, io_fetch
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _split_range(items: int, parts: int):
|
||||||
|
"""Разбиение range(items) на parts почти равных кусков (списки item'ов)."""
|
||||||
|
# TODO: верните список из parts списков номеров запросов
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def run_io_threads_manual(items: int, delay: float = 0.05, p: int = 4):
|
||||||
|
"""I/O-нагрузка на p ручных потоках.
|
||||||
|
|
||||||
|
Каждый поток обрабатывает СВОЙ кусок и кладёт результаты в СВОЙ список.
|
||||||
|
Вернуть (список_результатов, elapsed_seconds), упорядочено по item.
|
||||||
|
|
||||||
|
Подсказки:
|
||||||
|
- threading.Thread(target=worker, args=(...));
|
||||||
|
- start() всех потоков ДО первого join() — иначе выйдет последовательно.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def run_io_threads_pool(items: int, delay: float = 0.05, p: int = 4):
|
||||||
|
"""I/O-нагрузка через ThreadPoolExecutor(max_workers=p) и pool.map.
|
||||||
|
|
||||||
|
Вернуть (список_результатов, elapsed_seconds), упорядочено по item.
|
||||||
|
pool.map сохраняет порядок входа независимо от порядка завершения.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
items, delay = v["io"]["items"], v["io"]["delay"]
|
||||||
|
print(f"Вариант {v['variant']}: I/O-нагрузка {items} запросов по {delay} c\n")
|
||||||
|
|
||||||
|
rows = {"ручные потоки": [], "пул потоков": []}
|
||||||
|
for p in v["p_list"]:
|
||||||
|
t1, (res, _) = time_call(run_io_threads_manual, (items, delay, p), repeats=1)
|
||||||
|
assert [r[0] for r in res] == list(range(items)), "порядок нарушен!"
|
||||||
|
rows["ручные потоки"].append((p, t1))
|
||||||
|
|
||||||
|
t2, (res, _) = time_call(run_io_threads_pool, (items, delay, p), repeats=1)
|
||||||
|
assert [r[0] for r in res] == list(range(items)), "порядок нарушен!"
|
||||||
|
rows["пул потоков"].append((p, t2))
|
||||||
|
|
||||||
|
ideal = items * delay / p
|
||||||
|
print(f"p={p}: вручную {t1:.3f} c | пул {t2:.3f} c | идеал ≈ {ideal:.3f} c")
|
||||||
|
|
||||||
|
print()
|
||||||
|
table_rows = [(f"пул, p={p}", t) for p, t in rows["пул потоков"]]
|
||||||
|
print(make_table(table_rows, title="Пул потоков"))
|
||||||
|
|
||||||
|
save_speedup_plot(
|
||||||
|
rows,
|
||||||
|
f"Ускорение I/O-нагрузки потоками ({items} запросов)",
|
||||||
|
os.path.join(os.path.dirname(__file__), "speedup.png"))
|
||||||
|
|
||||||
|
# TODO: в отчёт — почему ускорение близко к идеальному?
|
||||||
|
# TODO (по желанию, +1 балл): submit + as_completed, порядок завершения
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Лабораторная работа 2. Многопоточный код (I/O-bound)
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары. **Зависимости:** лаба 1.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Освоить потоки на задаче, где они реально дают ускорение — I/O-bound.
|
||||||
|
К концу работы вы должны уметь объяснить, почему один и тот же механизм
|
||||||
|
бесполезен для CPU-задач (лаба 3).
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Что такое поток
|
||||||
|
|
||||||
|
Поток (thread) — независимая последовательность инструкций **внутри одного
|
||||||
|
процесса**. Все потоки делят общую память: один и тот же список, словарь,
|
||||||
|
глобальную переменную видно из любого потока. Это делает потоки лёгкими
|
||||||
|
(создание ~десятки микросекунд, у процессов — миллисекунды) и опасными
|
||||||
|
одновременно: два потока могут менять одну переменную без ведома друг друга
|
||||||
|
(лаба 5).
|
||||||
|
|
||||||
|
### Почему потоки ускоряют I/O-bound, но не CPU-bound
|
||||||
|
|
||||||
|
Когда поток выполняет `time.sleep`, чтение сокета или диска, он **отпускает
|
||||||
|
процессор** — ОС переключает его на другой поток. Десять потоков, ждущих сеть,
|
||||||
|
«спят» одновременно, и суммарное время ≈ времени одного запроса.
|
||||||
|
|
||||||
|
Когда поток считает (`x = a * b`), он держит процессор, а в CPython — ещё и
|
||||||
|
**GIL** (Global Interpreter Lock): глобальный замок, разрешающий исполнять
|
||||||
|
байт-код Python только одному потоку процесса. Поэтому CPU-код потоками не
|
||||||
|
ускоряется в принципе (подробно — лаба 3).
|
||||||
|
|
||||||
|
Правило: **потоки — для ожидания, процессы — для вычислений.**
|
||||||
|
|
||||||
|
### threading vs concurrent.futures
|
||||||
|
|
||||||
|
`threading.Thread` — ручное управление: создать, `start()`, `join()`,
|
||||||
|
самому разложить данные и собрать результаты. Полезно понять один раз.
|
||||||
|
|
||||||
|
`concurrent.futures.ThreadPoolExecutor` — пул потоков и высокоуровневый
|
||||||
|
интерфейс:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with ThreadPoolExecutor(max_workers=p) as pool:
|
||||||
|
results = list(pool.map(io_fetch, items)) # порядок входа сохранён
|
||||||
|
futures = [pool.submit(io_fetch, i) for i in items] # или Future-объекты
|
||||||
|
```
|
||||||
|
|
||||||
|
`submit` возвращает `Future` — «обещание» результата: `.result()` блокирует
|
||||||
|
до готовности, `.done()` проверяет готовность, можно навесить колбэк
|
||||||
|
`add_done_callback`. `pool.map` — как встроенный `map`, но параллельный;
|
||||||
|
результаты приходят в порядке входа.
|
||||||
|
|
||||||
|
В работе используются оба способа: `Thread` — чтобы понять механику,
|
||||||
|
`ThreadPoolExecutor` — как рабочий инструмент.
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
I/O-нагрузка та же, что в лабе 1 (`io_fetch` из `common.tasks`: имитация
|
||||||
|
запроса задержкой 0.05 c). Параметры items — из вашего варианта.
|
||||||
|
|
||||||
|
Заполните `solution.py`:
|
||||||
|
|
||||||
|
1. **`run_io_threads_manual(items, delay, p)`** — разбить `range(items)` на
|
||||||
|
`p` кусков, на каждый кусок — свой `threading.Thread`. Каждый поток
|
||||||
|
кладёт результаты в **свой** список (общая структура без синхронизации —
|
||||||
|
пока избегаем). Собрать куски по порядку.
|
||||||
|
2. **`run_io_threads_pool(items, delay, p)`** — то же через
|
||||||
|
`ThreadPoolExecutor(max_workers=p)` и `pool.map`.
|
||||||
|
3. **`main()`** — замерить T(1), T(2), T(4) (и T(8), если есть в варианте)
|
||||||
|
обоими способами; построить таблицу и график ускорения
|
||||||
|
(`common.benchmark`).
|
||||||
|
|
||||||
|
### Ожидаемые результаты (проверьте себя)
|
||||||
|
|
||||||
|
- T(1) ≈ items × delay (например 24 × 0.05 = 1.2 c);
|
||||||
|
- T(4) ≈ items × delay / 4 (если items кратно 4) — почти идеальное ускорение;
|
||||||
|
- ручные потоки и пул дают примерно одинаковое время.
|
||||||
|
|
||||||
|
Если T(4) ≈ T(1) — вы где-то синхронизируете лишний раз или запускаете
|
||||||
|
потоки по очереди (проверьте: `start()` всех потоков должен быть **до**
|
||||||
|
первого `join()`).
|
||||||
|
|
||||||
|
### Дополнительно (по желанию, +1 балл)
|
||||||
|
|
||||||
|
- `ThreadPoolExecutor` + `submit`/`as_completed`: выводить номер завершившегося
|
||||||
|
запроса по мере готовности — увидите, что порядок завершения не совпадает
|
||||||
|
с порядком запуска.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Почему `time.sleep` в 4 потоках занимает те же 0.05 c, а не 0.2 c?
|
||||||
|
2. Чем `Future` отличается от готового результата? Что делает `.result()`?
|
||||||
|
3. Почему потоки имеют общую память и чем это опасно?
|
||||||
|
4. Что произойдёт с вашей I/O-задачей, если `max_workers=100`? Почему рост
|
||||||
|
останавливается?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: таблица T(p) обоих способов, график S(p), анализ
|
||||||
|
(совпало ли с ожидаемым, где видны накладные расходы на создание потоков),
|
||||||
|
ответы на контрольные вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| Ручные потоки: результаты корректны и упорядочены | 2 |
|
||||||
|
| Пул потоков: результаты корректны | 2 |
|
||||||
|
| Замеры T(1/2/4), ускорение ≈ p на I/O-части | 2 |
|
||||||
|
| График и анализ в отчёте | 2 |
|
||||||
|
| Ответы на контрольные вопросы | 2 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- `start()` и сразу `join()` в одном цикле — потоки выполнятся по очереди.
|
||||||
|
- Общий список без блокировки (`results.append(...)` из многих потоков) —
|
||||||
|
в этой лабе «повезёт» (append атомарен из-за GIL), но полагаться на это
|
||||||
|
нельзя — в лабе 5 это сломают демонстративно.
|
||||||
|
- Забыли `args=(chunk,)` в `Thread` — функция вызвалась в главном потоке.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: GIL изнутри — интервал переключения и stat.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab03_gil/examples/01_gil_basics_demo.py
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
- sys.getswitchinterval() — как часто потоки уступают GIL;
|
||||||
|
- sys._is_gil_enabled() (Python 3.13+) — статус GIL;
|
||||||
|
- поток-«наблюдатель» работает, пока главный поток занят счётом
|
||||||
|
(переключения есть!), но суммарная производительность не растёт.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
|
||||||
|
def burn(n):
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
s += i ** 0.5
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Python: {sys.version.split()[0]}")
|
||||||
|
if hasattr(sys, "_is_gil_enabled"):
|
||||||
|
print(f"GIL включён: {sys._is_gil_enabled()}")
|
||||||
|
print(f"интервал переключения GIL: {sys.getswitchinterval() * 1000:.0f} мс\n")
|
||||||
|
|
||||||
|
N = 4_000_000
|
||||||
|
|
||||||
|
# 1. Один поток
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
burn(N)
|
||||||
|
t1 = time.perf_counter() - t0
|
||||||
|
print(f"1 поток: {t1:.3f} c")
|
||||||
|
|
||||||
|
# 2. Поток-наблюдатель: он живёт (переключения идут), но работе это не помогает
|
||||||
|
ticks = [0]
|
||||||
|
|
||||||
|
def observer(stop_flag):
|
||||||
|
while not stop_flag[0]:
|
||||||
|
ticks[0] += 1
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
stop = [False]
|
||||||
|
obs = threading.Thread(target=observer, args=(stop,), daemon=True)
|
||||||
|
obs.start()
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
burn(N) # главный поток считает
|
||||||
|
t2 = time.perf_counter() - t0
|
||||||
|
stop[0] = True
|
||||||
|
obs.join()
|
||||||
|
print(f"1 поток + наблюдатель: {t2:.3f} c (наблюдатель проснулся "
|
||||||
|
f"{ticks[0]} раз) — работа не замедлилась: переключения дешёвые")
|
||||||
|
|
||||||
|
# 3. Два считающих потока: время то же, работы вдвое больше — но не быстрее
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
threads = [threading.Thread(target=burn, args=(N // 2,)) for _ in range(2)]
|
||||||
|
for t in threads:
|
||||||
|
t.start()
|
||||||
|
for t in threads:
|
||||||
|
t.join()
|
||||||
|
t4 = time.perf_counter() - t0
|
||||||
|
print(f"2 потока по {N // 2}: {t4:.3f} c => S={t1 / t4:.2f} (GIL!)")
|
||||||
|
print("\nВывод: переключения GIL происходят, но байт-код всё равно")
|
||||||
|
print("исполняется по очереди — суммарная скорость не растёт.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: hashlib освобождает GIL — потоки дают настоящее ускорение.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab03_gil/examples/02_hashlib_gil_demo.py
|
||||||
|
|
||||||
|
hashlib.sha256 отпускает GIL на время хеширования ОДНОГО блока. На коротких
|
||||||
|
блоках (4 КБ) вызовы слишком быстрые — GIL забирается обратно, ускорения нет.
|
||||||
|
На больших блоках (1 МБ) вызовы длинные — потоки работают параллельно.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
# Три набора данных (для hashlib — одинаковый суммарный объём, разные блоки)
|
||||||
|
PY_DATA = [os.urandom(256 * 1024) for _ in range(16)] # 4 МБ, для чистого Python
|
||||||
|
HASH_BIG = [os.urandom(1 << 20) for _ in range(48)] # 48 МБ блоками по 1 МБ
|
||||||
|
HASH_SMALL = [os.urandom(4096) for _ in range(48 * 256)] # те же 48 МБ блоками по 4 КБ
|
||||||
|
|
||||||
|
MASK = 0xFFFFFFFFFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def py_checksum(chunk):
|
||||||
|
"""Чистый Python: GIL не отпускает."""
|
||||||
|
acc = 0
|
||||||
|
for b in chunk:
|
||||||
|
for x in b: # питоновский цикл по байтам
|
||||||
|
acc = (acc + x * 31) & MASK
|
||||||
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
def hash_checksum(chunk):
|
||||||
|
"""C-функция hashlib: GIL отпускается на время хеширования блока."""
|
||||||
|
acc = 0
|
||||||
|
h = hashlib.sha256
|
||||||
|
for b in chunk:
|
||||||
|
acc = (acc + int.from_bytes(h(b).digest()[:8], "big")) & MASK
|
||||||
|
return acc
|
||||||
|
|
||||||
|
|
||||||
|
def run_threads(fn, data, parts):
|
||||||
|
bounds = [i * len(data) // parts for i in range(parts + 1)]
|
||||||
|
chunks = [data[bounds[i]:bounds[i + 1]] for i in range(parts)]
|
||||||
|
with ThreadPoolExecutor(max_workers=parts) as pool:
|
||||||
|
return list(pool.map(fn, chunks))
|
||||||
|
|
||||||
|
|
||||||
|
def min_time(fn, data, parts, repeats=3):
|
||||||
|
best = float("inf")
|
||||||
|
for _ in range(repeats):
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
run_threads(fn, data, parts)
|
||||||
|
best = min(best, time.perf_counter() - t0)
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def bench(label, fn, data):
|
||||||
|
acc1 = sum(run_threads(fn, data, 1)) & MASK
|
||||||
|
acc4 = sum(run_threads(fn, data, 4)) & MASK
|
||||||
|
assert acc1 == acc4, "результат зависит от разбиения — ошибка!"
|
||||||
|
t1 = min_time(fn, data, 1)
|
||||||
|
t4 = min_time(fn, data, 4)
|
||||||
|
print(f"{label:<26} T(1)={t1:.3f} c T(4)={t4:.3f} c S(4)={t1 / t4:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("hashlib: одинаковый суммарный объём 48 МБ, разные размеры блоков\n")
|
||||||
|
bench("чистый Python", py_checksum, PY_DATA)
|
||||||
|
bench("hashlib, блоки 4 КБ", hash_checksum, HASH_SMALL)
|
||||||
|
bench("hashlib, блоки 1 МБ", hash_checksum, HASH_BIG)
|
||||||
|
print("\nS(4)≈1 у чистого Python и мелких блоков против S(4)≈ядрам у")
|
||||||
|
print("больших блоков. GIL отпускается на время ОДНОГО вызова C-функции —")
|
||||||
|
print("вызовы должны быть длинными.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: sys.setswitchinterval — интервал переключения GIL.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab03_gil/examples/03_switch_interval_demo.py
|
||||||
|
|
||||||
|
Уменьшение интервала переключения не добавляет параллельности (GIL всё равно
|
||||||
|
один), но увеличивает накладные расходы: слишком частые переключения
|
||||||
|
замедляют работу.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
|
||||||
|
def burn(n):
|
||||||
|
s = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
s += i ** 0.5
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def bench(interval, n_threads, n_total):
|
||||||
|
sys.setswitchinterval(interval)
|
||||||
|
bounds = [i * n_total // n_threads for i in range(n_threads + 1)]
|
||||||
|
chunks = [bounds[i + 1] - bounds[i] for i in range(n_threads)]
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with ThreadPoolExecutor(max_workers=n_threads) as pool:
|
||||||
|
pool.map(burn, chunks)
|
||||||
|
return time.perf_counter() - t0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
N = 4_000_000
|
||||||
|
print(f"{'интервал':>10} {'T(1 поток)':>12} {'T(4 потока)':>12}")
|
||||||
|
for interval in (0.005, 0.0005, 0.00005):
|
||||||
|
t1 = bench(interval, 1, N)
|
||||||
|
t4 = bench(interval, 4, N)
|
||||||
|
print(f"{interval * 1000:>9.1f}мс {t1:>11.3f}c {t4:>11.3f}c"
|
||||||
|
f" S(4)={t1 / t4:.2f}")
|
||||||
|
sys.setswitchinterval(0.005) # вернуть значение по умолчанию
|
||||||
|
print("\nВывод: интервал переключения влияет на накладные расходы,")
|
||||||
|
print("но параллельности не добавляет — держатель GIL всё равно один.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 3: GIL — почему потоки не ускоряют CPU-код.
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab03.py).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab03_gil/solution.py
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import save_speedup_plot, time_call
|
||||||
|
from common.tasks import TASKS, get_variant
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
def run_task_on_threads(task_name: str, params: dict, p: int):
|
||||||
|
"""Выполнить CPU-задачу на p потоках (ThreadPoolExecutor).
|
||||||
|
|
||||||
|
Каждый поток получает свой кусок (task['split']) и выполняет task['kernel'].
|
||||||
|
Вернуть (checksum, elapsed_seconds). checksum обязан совпасть с
|
||||||
|
последовательным запуском — иначе разбиение/сборка неверны.
|
||||||
|
|
||||||
|
Подсказка: потоки, в отличие от процессов, не требуют пиккелируемости —
|
||||||
|
task["kernel"] можно вызывать прямо в воркере.
|
||||||
|
"""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
# TODO: build -> split -> pool.map(kernel, chunks) -> combine -> checksum
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def measure_cpu_threads(task_name: str, params: dict, p_list):
|
||||||
|
"""Замеры T(p) на потоках для чистого-Python задачи.
|
||||||
|
|
||||||
|
Вернуть dict {p: лучшее_время}. p=1 — база. Проверять совпадение
|
||||||
|
checksum при каждом p!
|
||||||
|
"""
|
||||||
|
# TODO: для каждого p -> time_call(run_task_on_threads, ..., repeats=3)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def measure_gil_release(p_list, block_size=1 << 20, blocks_per_part=32):
|
||||||
|
"""Замеры T(p) для hashlib (освобождает GIL на длинных вызовах).
|
||||||
|
|
||||||
|
Готовит данные заранее: W = blocks_per_part * max(p_list) блоков по
|
||||||
|
block_size байт (os.urandom). Общий объём W делится между p потоками —
|
||||||
|
как в CPU-замерах. Вернуть dict {p: лучшее_время}, checksum проверять.
|
||||||
|
|
||||||
|
ВАЖНО: блоки должны быть большими (≥ 256 КБ). На коротких вызовах GIL
|
||||||
|
возвращается мгновенно и ускорение пропадает. Сравните block_size=4096
|
||||||
|
и block_size=1<<20 в отчёте!
|
||||||
|
"""
|
||||||
|
# TODO: данные -> деление на p кусков -> ThreadPoolExecutor ->
|
||||||
|
# hashlib.sha256(блок).digest() -> checksum -> {p: время}
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def gil_status():
|
||||||
|
"""Вернуть (версия_python, gil_enabled: bool).
|
||||||
|
|
||||||
|
Используйте sys._is_gil_enabled() (Python 3.13+); если атрибута нет,
|
||||||
|
считайте GIL включённым.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
print(f"Вариант {v['variant']}: {v['describe']}\n")
|
||||||
|
|
||||||
|
ver, gil = gil_status()
|
||||||
|
print(f"Python {ver}, GIL включён: {gil}\n")
|
||||||
|
|
||||||
|
# --- 1. Чистый Python: S(p) ≈ 1 ---------------------------------------
|
||||||
|
t_cpu = measure_cpu_threads(v["task_name"], v["cpu_params"], v["p_list"])
|
||||||
|
print("Чистый Python (GIL не отпускается):")
|
||||||
|
base = t_cpu[1]
|
||||||
|
for p, t in sorted(t_cpu.items()):
|
||||||
|
print(f" p={p}: T={t:.3f} c S={base / t:.2f}")
|
||||||
|
|
||||||
|
# --- 2. hashlib: S(p) > 1 ---------------------------------------------
|
||||||
|
t_hash = measure_gil_release(v["p_list"])
|
||||||
|
print("\nhashlib SHA-256 (GIL отпускается, блоки 1 МБ):")
|
||||||
|
base = t_hash[1]
|
||||||
|
for p, t in sorted(t_hash.items()):
|
||||||
|
print(f" p={p}: T={t:.3f} c S={base / t:.2f}")
|
||||||
|
|
||||||
|
# --- 2b. Мелкие блоки: ускорение должно исчезнуть ----------------------
|
||||||
|
t_small = measure_gil_release(v["p_list"], block_size=4096,
|
||||||
|
blocks_per_part=8000)
|
||||||
|
print("\nhashlib SHA-256 (блоки 4 КБ — GIL возвращается сразу):")
|
||||||
|
base = t_small[1]
|
||||||
|
for p, t in sorted(t_small.items()):
|
||||||
|
print(f" p={p}: T={t:.3f} c S={base / t:.2f}")
|
||||||
|
|
||||||
|
# --- 3. График ----------------------------------------------------------
|
||||||
|
save_speedup_plot(
|
||||||
|
{"чистый Python": sorted(t_cpu.items()),
|
||||||
|
"hashlib 1 МБ (released GIL)": sorted(t_hash.items()),
|
||||||
|
"hashlib 4 КБ": sorted(t_small.items())},
|
||||||
|
f"GIL: ускорение потоками (Python {ver})",
|
||||||
|
os.path.join(os.path.dirname(__file__), "speedup.png"))
|
||||||
|
|
||||||
|
# TODO: в отчёт — таблицу серий, график, объяснение разницы кривых,
|
||||||
|
# ответы на контрольные вопросы из методички.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Лабораторная работа 3. GIL: почему потоки не ускоряют CPU-код
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары. **Зависимости:** лабы 1–2.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Экспериментально исследовать GIL (Global Interpreter Lock) и научиться
|
||||||
|
обходить его тремя способами: процессы, освобождающие GIL C-функции
|
||||||
|
(`hashlib`), мультипоточность вне GIL (превью `sys._is_gil_enabled`).
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Что такое GIL
|
||||||
|
|
||||||
|
GIL — мьютекс в CPython, разрешающий исполнять байт-код Python только одному
|
||||||
|
потоку процесса в каждый момент. Один интерпретатор — один «держатель» GIL.
|
||||||
|
|
||||||
|
Зачем он нужен: упрощает реализацию CPython (счётчики ссылок, внутренние
|
||||||
|
структуры не защищены отдельными замками) и делает однопоточный код быстрым.
|
||||||
|
Потоки в CPython — **настоящие** потоки ОС, но байт-код Python они исполняют
|
||||||
|
по очереди.
|
||||||
|
|
||||||
|
Как поток получает процессор: держатель GIL обязан отпускать его
|
||||||
|
периодически (по умолчанию каждые ~5 мс, `sys.getswitchinterval()`), и при
|
||||||
|
блокирующих вызовах (`sleep`, чтение сети/диска).
|
||||||
|
|
||||||
|
### Следствия
|
||||||
|
|
||||||
|
| Тип задачи | Потоки | Почему |
|
||||||
|
|---|---|---|
|
||||||
|
| I/O-bound | ускоряют почти идеально | GIL отпускается на время ожидания |
|
||||||
|
| CPU-bound (чистый Python) | НЕ ускоряют | GIL: байт-код исполняется по очереди |
|
||||||
|
| CPU-bound (C-код внутри) | МОГУТ ускорять | C-функция может отпустить GIL |
|
||||||
|
|
||||||
|
Третий пункт — самый интересный: библиотеки на C (`hashlib`, `zlib`,
|
||||||
|
`numpy` во многих операциях) во время длинных вычислений **отпускают GIL**,
|
||||||
|
и потоки работают по-настоящему параллельно. Тонкость: GIL отпускается на
|
||||||
|
время **одного вызова** C-функции. Если блоки маленькие (4 КБ), вызовы
|
||||||
|
слишком короткие и GIL тут же забирается обратно — ускорения не будет.
|
||||||
|
Блоки ≥ 256 КБ (в работе — 1 МБ) дают честный параллелизм. В лабе это
|
||||||
|
демонстрирует `hashlib`.
|
||||||
|
|
||||||
|
### Свободнный threading (Python 3.13+, превью)
|
||||||
|
|
||||||
|
Начиная с CPython 3.13 существует экспериментальная сборка без GIL
|
||||||
|
(free-threaded). В обычном CPython можно проверить, включён ли GIL:
|
||||||
|
`sys._is_gil_enabled()`. В работе — короткое ознакомление, не основное.
|
||||||
|
|
||||||
|
### Проверка GIL в эксперименте
|
||||||
|
|
||||||
|
Метод: фиксированная CPU-работа `W`. Замеряем T(1), затем ту же работу
|
||||||
|
на p потоках. Ускорение S = T(1)/T(p):
|
||||||
|
- S ≈ 1 → GIL не отпускался (чистый Python);
|
||||||
|
- S ≈ p → GIL отпускался (C-код, sleep).
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
CPU-задача — из вашего варианта (`common.tasks`), ядро `kernel` работает на
|
||||||
|
чистом Python. Заполните `solution.py`:
|
||||||
|
|
||||||
|
1. **`measure_cpu_threads(task_name, params, p_list)`** — для каждого p:
|
||||||
|
последовательный запуск (p=1, база) и запуск CPU-задачи на p **потоках**
|
||||||
|
(`ThreadPoolExecutor`, каждый поток — свой кусок). Вернуть
|
||||||
|
`{p: время}`. Убедиться, что checksum совпадает с последовательным.
|
||||||
|
2. **`measure_gil_release(p_list, block_size, blocks_per_part)`** — то же,
|
||||||
|
но работа — SHA-256 над готовыми блоками байтов (освобождает GIL).
|
||||||
|
Фиксированный общий объём `W = blocks_per_part * max(p_list)` блоков по
|
||||||
|
`block_size` байт (`os.urandom`), делится между p потоками. Вернуть
|
||||||
|
`{p: время}`, проверять checksum.
|
||||||
|
3. **`gil_status()`** — вернуть `(python_version, gil_enabled)` через
|
||||||
|
`sys._is_gil_enabled()` (если атрибута нет — GIL включён).
|
||||||
|
4. **`main()`** — таблицы ускорений для обоих случаев + график S(p).
|
||||||
|
|
||||||
|
### Ожидаемые результаты
|
||||||
|
|
||||||
|
- Чистый Python: S(4) ≈ 0.9–1.1 (замедление из-за переключений GIL).
|
||||||
|
- SHA-256 на блоках 1 МБ: S(4) ≈ 2–4 (реальный параллелизм).
|
||||||
|
- SHA-256 на блоках 4 КБ: S(4) ≈ 0.8 — GIL не успевает отпускаться.
|
||||||
|
**Попробуйте оба размера блоков и объясните разницу** — это ядро работы.
|
||||||
|
- Разница между «чистый Python» и «hashlib 1 МБ» — и есть GIL.
|
||||||
|
|
||||||
|
### Важно про честность замеров
|
||||||
|
|
||||||
|
- объём работы при любом p одинаковый (проверка checksum);
|
||||||
|
- `time_call(..., repeats=3)`, берём лучшее;
|
||||||
|
- следите, чтобы данные hashing-задачи были крупными блоками (в банке
|
||||||
|
задач так и сделано).
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Что такое GIL и зачем он в CPython?
|
||||||
|
2. Почему `time.sleep` в 100 потоках не «съедает» процессор, а 100
|
||||||
|
считающих потоков работают как один?
|
||||||
|
3. Почему `hashlib.sha256` ускоряется потоками, а `sum` по списку — нет?
|
||||||
|
4. Что изменится в Python 3.13+ free-threaded сборках? Найдите статус PEP 703.
|
||||||
|
5. Как проверить в вашем эксперименте, что работа распределена честно?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: таблица S(p) для чистого Python и для hashlib,
|
||||||
|
график с обеими кривыми и «идеальной» прямой, вывод: когда потоки полезны.
|
||||||
|
Ответы на контрольные вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| Корректный замер чистого Python (S ≈ 1) | 2 |
|
||||||
|
| Корректный замер hashlib (S(p) > 1.5 при p=4) | 2 |
|
||||||
|
| `gil_status` и версия Python в отчёте | 1 |
|
||||||
|
| График с тремя кривыми | 2 |
|
||||||
|
| Анализ и ответы на вопросы | 3 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- Слишком мелкие блоки для hashlib (4 КБ): S ≈ 0.8, а не 3+. GIL отпускается
|
||||||
|
на время одного вызова — вызовы должны быть долгими (блоки ≥ 256 КБ).
|
||||||
|
- Разный объём работы при разных p — обязательно проверяйте checksum.
|
||||||
|
- Замер на ноутбуке без питания — турбо-частоты скачут, S(p) «плавает».
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: spawn — каждый процесс заново импортирует модуль.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab04_multiprocessing/examples/01_spawn_demo.py
|
||||||
|
|
||||||
|
Показывает, что на Windows/spawn дочерний процесс заново выполняет
|
||||||
|
модуль верхнего уровня. Поэтому защита __main__ обязательна.
|
||||||
|
Здесь же видно время старта процесса.
|
||||||
|
"""
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
print(f"[импорт модуля] pid={os.getpid()}, parent={os.getppid()}, "
|
||||||
|
f"метод={multiprocessing.get_start_method()}")
|
||||||
|
|
||||||
|
|
||||||
|
def worker(x):
|
||||||
|
return x * x
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"\n[главный процесс] pid={os.getpid()}")
|
||||||
|
ctx = multiprocessing.get_context() # spawn на Windows и macOS
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with ctx.Pool(2) as pool:
|
||||||
|
results = pool.map(worker, range(8))
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
|
||||||
|
print(f"pool.map -> {results}")
|
||||||
|
print(f"время (включая старт 2 процессов): {dt * 1000:.1f} мс")
|
||||||
|
print("\nОбратите внимание: строка [импорт модуля] могла напечататься")
|
||||||
|
print("дважды-трижды (для каждого воркера при spawn). На fork — один раз.")
|
||||||
|
print("Вывод: старт воркеров не бесплатный — это накладные расходы лабы 4.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: что можно, а что нельзя передать в Pool (pickle).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab04_multiprocessing/examples/02_pickle_demo.py
|
||||||
|
|
||||||
|
Вложенные функции и лямбды не пиккелятся — PicklingError.
|
||||||
|
Модульные функции — можно. Отсюда правило: kernel для Pool живёт
|
||||||
|
на уровне модуля (как dispatch_kernel в common.tasks).
|
||||||
|
"""
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
from common.tasks import dispatch_kernel, smoke_params
|
||||||
|
|
||||||
|
|
||||||
|
def module_level_double(x): # МОЖНО: функция уровня модуля
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
|
||||||
|
def make_nested():
|
||||||
|
def nested_double(x): # НЕЛЬЗЯ: вложенная функция
|
||||||
|
return x * 2
|
||||||
|
return nested_double
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ctx = multiprocessing.get_context()
|
||||||
|
|
||||||
|
print("--- модульная функция: работает ---")
|
||||||
|
with ctx.Pool(2) as pool:
|
||||||
|
print(f" pool.map(module_level_double, [1,2,3]) = "
|
||||||
|
f"{pool.map(module_level_double, [1, 2, 3])}")
|
||||||
|
|
||||||
|
print("\n--- lambda: PicklingError ---")
|
||||||
|
try:
|
||||||
|
with ctx.Pool(2) as pool:
|
||||||
|
pool.map(lambda x: x * 2, [1, 2, 3])
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {type(e).__name__}: {str(e)[:90]}...")
|
||||||
|
|
||||||
|
print("\n--- kernel задачи через dispatch_kernel: работает ---")
|
||||||
|
task_name = "primes"
|
||||||
|
chunks = None
|
||||||
|
task = __import__("common.tasks", fromlist=["TASKS"]).TASKS[task_name]
|
||||||
|
data = task["build"](smoke_params(task_name))
|
||||||
|
chunks = task["split"](data, 2)
|
||||||
|
payload = [(task_name, ch) for ch in chunks]
|
||||||
|
with ctx.Pool(2) as pool:
|
||||||
|
partials = pool.map(dispatch_kernel, payload)
|
||||||
|
print(f" подсчёт простых ≤ 2000: {task['combine'](partials)} "
|
||||||
|
f"(ожидалось 303)")
|
||||||
|
|
||||||
|
print("\nВывод: всё, что уходит воркерам, должно пиккелиться —")
|
||||||
|
print("поэтому в этом курсе kernel диспетчеризуется по имени задачи.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: Process + Queue — ручная организация воркеров.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab04_multiprocessing/examples/03_process_queue_demo.py
|
||||||
|
|
||||||
|
Pool — удобный «конвейер». Process+Queue — ручная схема: воркеры сами берут
|
||||||
|
задачи из очереди. Это основа для producer-consumer в лабе 5.
|
||||||
|
"""
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
|
|
||||||
|
|
||||||
|
def worker(task_q, result_q):
|
||||||
|
"""Воркер: берёт задачи, пока не встретит None — «маркер конца»."""
|
||||||
|
name = multiprocessing.current_process().name
|
||||||
|
while True:
|
||||||
|
task = task_q.get()
|
||||||
|
if task is None:
|
||||||
|
result_q.put((name, "done"))
|
||||||
|
break
|
||||||
|
idx, value = task
|
||||||
|
time.sleep(0.05) # «полезная работа»
|
||||||
|
result_q.put((name, f"задача {idx}: {value}² = {value ** 2}"))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ctx = multiprocessing.get_context()
|
||||||
|
task_q = ctx.Queue()
|
||||||
|
result_q = ctx.Queue()
|
||||||
|
|
||||||
|
n_workers = 3
|
||||||
|
workers = [ctx.Process(target=worker, args=(task_q, result_q),
|
||||||
|
name=f"воркер-{i}")
|
||||||
|
for i in range(n_workers)]
|
||||||
|
for w in workers:
|
||||||
|
w.start()
|
||||||
|
|
||||||
|
# кладём задачи
|
||||||
|
for i in range(9):
|
||||||
|
task_q.put((i, i + 1))
|
||||||
|
# маркеры конца — по одному на воркер
|
||||||
|
for _ in range(n_workers):
|
||||||
|
task_q.put(None)
|
||||||
|
|
||||||
|
# собираем результаты
|
||||||
|
done = 0
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
while done < n_workers:
|
||||||
|
name, msg = result_q.get()
|
||||||
|
if msg == "done":
|
||||||
|
done += 1
|
||||||
|
print(f" [{name}] завершился")
|
||||||
|
else:
|
||||||
|
print(f" [{name}] {msg}")
|
||||||
|
for w in workers:
|
||||||
|
w.join()
|
||||||
|
print(f"время: {time.perf_counter() - t0:.2f} c (9 задач по 0.05 c на 3 воркерах)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 4: мультипроцессорный код.
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab04.py, запускаются через подпроцесс).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab04_multiprocessing/solution.py
|
||||||
|
"""
|
||||||
|
import multiprocessing
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import make_table, save_speedup_plot, time_call
|
||||||
|
from common.tasks import TASKS, dispatch_kernel, get_variant
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _sum_list(data):
|
||||||
|
"""Функция уровня модуля — суммирует список (для measure_overhead)."""
|
||||||
|
return sum(data)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cpu_processes(task_name: str, params: dict, p: int):
|
||||||
|
"""CPU-задача на p процессах через Pool и dispatch_kernel.
|
||||||
|
|
||||||
|
Вернуть (checksum, elapsed_seconds). Подсказки:
|
||||||
|
- payload = [(task_name, chunk) для каждого куска];
|
||||||
|
- dispatch_kernel пиккелится, лямбды и вложенные функции — НЕТ;
|
||||||
|
- ctx = multiprocessing.get_context(); with ctx.Pool(p) as pool: ...
|
||||||
|
"""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
# TODO: build -> split -> pool.map(dispatch_kernel, payload) ->
|
||||||
|
# combine -> checksum (замерить время)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def measure_overhead(items_count_list):
|
||||||
|
"""Стоимость передачи данных процессу.
|
||||||
|
|
||||||
|
Для каждого n из items_count_list: отправить в воркер список из n чисел
|
||||||
|
(list(range(n))), воркер возвращает их сумму (_sum_list). Пул создаётся
|
||||||
|
ОДИН раз, первый вызов — прогрев. Вернуть {n: лучшее_время из 3}.
|
||||||
|
"""
|
||||||
|
# TODO: ctx.Pool(1) -> для каждого n: замер pool.map(_sum_list, [data])
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# --- smoke-режим для автотестов: быстрый прогон и JSON-отчёт -----------
|
||||||
|
# (не удаляйте: тесты запускают solution.py подпроцессом с LAB_SMOKE=1)
|
||||||
|
if os.environ.get("LAB_SMOKE") == "1":
|
||||||
|
from common.tasks import run_sequential, smoke_params
|
||||||
|
name = "integrate" # smoke не зависит от варианта: быстрая задача
|
||||||
|
sp = smoke_params(name)
|
||||||
|
expected, _ = run_sequential(name, sp, parts=1)
|
||||||
|
got, _t = run_cpu_processes(name, sp, p=2)
|
||||||
|
ok = (got == expected) or (isinstance(got, float) and
|
||||||
|
abs(got - expected) < 1e-6 * max(1, abs(expected)))
|
||||||
|
ov = measure_overhead([1_000, 50_000])
|
||||||
|
print("LAB_SMOKE_JSON:" + json.dumps({
|
||||||
|
"ok": True, "result_ok": bool(ok), "p": 2,
|
||||||
|
"overhead": {str(k): t for k, t in ov.items()}}))
|
||||||
|
return
|
||||||
|
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||||
|
print(f"метод запуска: {multiprocessing.get_start_method()}\n")
|
||||||
|
|
||||||
|
# --- T(p) на процессах ---------------------------------------------------
|
||||||
|
rows, times = [], {}
|
||||||
|
expected = None
|
||||||
|
for p in v["p_list"]:
|
||||||
|
t, (cs, _) = time_call(run_cpu_processes,
|
||||||
|
(v["task_name"], v["cpu_params"], p), repeats=2)
|
||||||
|
times[p] = t
|
||||||
|
rows.append((f"процессы, p={p}", t))
|
||||||
|
if expected is None:
|
||||||
|
expected = cs
|
||||||
|
assert cs == expected or (isinstance(cs, float) and
|
||||||
|
abs(cs - expected) < 1e-6 * max(1, abs(expected))), \
|
||||||
|
f"checksum при p={p} не совпал!"
|
||||||
|
print(make_table(rows, title="CPU-задача на процессах"))
|
||||||
|
|
||||||
|
base = times[1]
|
||||||
|
for p, t in times.items():
|
||||||
|
print(f" S(p={p}) = {base / t:.2f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# --- Накладные расходы передачи -------------------------------------------
|
||||||
|
overhead = measure_overhead([1_000, 100_000, 1_000_000])
|
||||||
|
print("Стоимость передачи данных процессу (туда+обратно):")
|
||||||
|
for n, t in sorted(overhead.items()):
|
||||||
|
print(f" {n:>9,} чисел: {t * 1000:8.1f} мс".replace(",", " "))
|
||||||
|
|
||||||
|
save_speedup_plot({"процессы": sorted(times.items())},
|
||||||
|
f"Ускорение CPU-задачи процессами ({v['describe']})",
|
||||||
|
os.path.join(os.path.dirname(__file__), "speedup.png"))
|
||||||
|
|
||||||
|
# TODO: в отчёт — S(p) и число физических ядер вашей машины,
|
||||||
|
# сравните время kernel варианта со временем передачи из measure_overhead.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Лабораторная работа 4. Мультипроцессорный код
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары. **Зависимости:** лабы 1–3.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Освоить настоящую параллельность на CPU-задачах: процессы обходят GIL.
|
||||||
|
Измерить накладные расходы multiprocessing и понять, когда они съедают
|
||||||
|
выигрыш.
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Процесс против потока
|
||||||
|
|
||||||
|
Процесс — отдельный экземпляр интерпретатора со **своей** памятью. У каждого
|
||||||
|
процесса свой GIL, поэтому CPU-задачи на p процессах исполняются по-настоящему
|
||||||
|
параллельно (на p ядрах). Цена: создание процесса ~10–100 мс (против ~0.1 мс
|
||||||
|
у потока), а любые данные передаются между процессами через **pickle** —
|
||||||
|
сериализацию. Большие данные (матрицы, списки миллионов чисел) передаются
|
||||||
|
долго, и это становится главным ограничением.
|
||||||
|
|
||||||
|
### Методы запуска: fork и spawn
|
||||||
|
|
||||||
|
- **fork** (Linux/macOS): дочерний процесс — копия родителя «снимком» памяти.
|
||||||
|
Быстро, но опасно с потоками и блокировками.
|
||||||
|
- **spawn** (Windows, по умолчанию на macOS): запускается новый интерпретатор,
|
||||||
|
который заново импортирует ваш модуль. Медленнее и строже: **весь код
|
||||||
|
создания процессов должен быть под `if __name__ == "__main__":`**, иначе
|
||||||
|
бесконечное размножение процессов.
|
||||||
|
|
||||||
|
Проверить и выбрать: `multiprocessing.get_start_method()` /
|
||||||
|
`multiprocessing.get_context("spawn")`.
|
||||||
|
|
||||||
|
### Pool и передаваемые функции
|
||||||
|
|
||||||
|
`multiprocessing.Pool(p)` — пул из p воркеров-процессов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with multiprocessing.Pool(p) as pool:
|
||||||
|
results = pool.map(kernel, chunks) # как map, но по процессам
|
||||||
|
```
|
||||||
|
|
||||||
|
ВАЖНО: `kernel` и `chunks` должны пиккелиться. Вложенные функции (def внутри
|
||||||
|
def) и лямбды — **не** пиккелятся. Поэтому в `common.tasks` ядро задачи
|
||||||
|
вызывается через модульный `dispatch_kernel((name, chunk))`.
|
||||||
|
|
||||||
|
### Что именно передаётся
|
||||||
|
|
||||||
|
Схема split → kernel → combine в мире процессов означает:
|
||||||
|
- `chunks` сериализуются и уходят воркерам (расход на передачу);
|
||||||
|
- частичные результаты сериализуются и возвращаются (расход на возврат);
|
||||||
|
- если данные больше результата — передача может занять дольше, чем счёт
|
||||||
|
(см. задачу «сортировка» в банке задач).
|
||||||
|
|
||||||
|
### Разные способы организации
|
||||||
|
|
||||||
|
- `Pool.map` — распределить список по воркерам;
|
||||||
|
- `Process` + `Queue` — ручная схема: воркеры берут задачи из очереди и кладут
|
||||||
|
ответы в другую (задел для лабы 5);
|
||||||
|
- `ProcessPoolExecutor` —ThreadPoolExecutor-подобный интерфейс для процессов.
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
CPU-задача — из вашего варианта. Заполните `solution.py`:
|
||||||
|
|
||||||
|
1. **`run_cpu_processes(task_name, params, p)`** — CPU-задача на `p`
|
||||||
|
процессах через `Pool` и `dispatch_kernel`. Вернуть `(checksum, сек)`.
|
||||||
|
Вся логика — под `if __name__ == "__main__"` (в `main()`).
|
||||||
|
2. **`measure_overhead(items_count)`** — измерить стоимость передачи:
|
||||||
|
отправить в воркер список из `items_count` чисел, воркер возвращает его
|
||||||
|
длину. Время / количество = цена байта. Вернуть `{items_count: сек}`.
|
||||||
|
3. **`main()`** — T(p) для p из варианта; таблица; график S(p) с «идеальной»
|
||||||
|
прямой; вывод о накладных расходах.
|
||||||
|
|
||||||
|
### Ожидаемые результаты
|
||||||
|
|
||||||
|
- CPU-задача: S(p) растёт почти линейно до числа физических ядер;
|
||||||
|
на машинах с гиперторингом S(p_max) может упираться в ~число физ. ядер.
|
||||||
|
- `measure_overhead`: передача маленьких данных ~миллисекунды, больших —
|
||||||
|
десятки-сотни мс. Сравните с временем kernel вашего варианта.
|
||||||
|
|
||||||
|
### Важно (Windows)
|
||||||
|
|
||||||
|
- Всё создание пулов — строго внутри `main()`/под `__main__`.
|
||||||
|
- Не используйте лямбды и вложенные функции как аргументы `pool.map`.
|
||||||
|
- На macOS/Linux можно сравнить `fork` и `spawn` (`get_context`) — добавьте
|
||||||
|
в отчёт, если работаете не на Windows.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Почему процессы ускоряют CPU-код, а потоки — нет?
|
||||||
|
2. Что такое pickle и почему лямбда не может быть аргументом `pool.map`?
|
||||||
|
3. Замерили T(p=8) на 4-ядерной машине с гиперторингом — ускорение ~4, а не 8.
|
||||||
|
Почему?
|
||||||
|
4. Когда передача данных между процессами дороже самих вычислений?
|
||||||
|
5. Почему на Windows нужен `if __name__ == "__main__":`?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: таблица T(p), график S(p), замеры overhead передачи,
|
||||||
|
анализ (до какого p растёт, где упирается), ответы на вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| Корректный запуск на процессах, checksum совпадает | 2 |
|
||||||
|
| S(p) > 1.5 при p=4 на CPU-задаче | 2 |
|
||||||
|
| Замер overhead передачи данных | 2 |
|
||||||
|
| График + анализ накладных расходов | 2 |
|
||||||
|
| Ответы на контрольные вопросы | 2 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- Нет `__main__` — на Windows бесконечный spawn (см. типичные_ошибки п.1).
|
||||||
|
- Лямбда/вложенная функция в `pool.map` — `PicklingError`.
|
||||||
|
- Слишком мелкие куски: создание процессов и передача дороже счёта,
|
||||||
|
S(p) < 1. Увеличьте размер задачи.
|
||||||
@@ -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` в другом порядке → дедлок
|
||||||
|
(порядок взятия замков должен быть единым).
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: asyncio — конкурентность на одном потоке.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab06_asyncio/examples/01_async_basics_demo.py
|
||||||
|
|
||||||
|
Три корутины «ждут сеть» одновременно: 0.6 c ожидания занимают 0.3 c.
|
||||||
|
Всё на одном потоке — переключения только на await.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch(name: str, delay: float) -> str:
|
||||||
|
print(f" {name}: старт")
|
||||||
|
await asyncio.sleep(delay) # «ожидание сети» — уступает циклу
|
||||||
|
print(f" {name}: готов")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("--- gather: три корутины конкурентно ---")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
results = await asyncio.gather(
|
||||||
|
fetch("A", 0.3),
|
||||||
|
fetch("B", 0.2),
|
||||||
|
fetch("C", 0.1),
|
||||||
|
)
|
||||||
|
print(f"результаты: {results}")
|
||||||
|
print(f"время: {time.perf_counter() - t0:.2f} c "
|
||||||
|
f"(сумма ожиданий 0.6 c)\n")
|
||||||
|
|
||||||
|
print("--- create_task: запустить сейчас, ждать позже ---")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
task = asyncio.create_task(fetch("фоновая", 0.3))
|
||||||
|
await asyncio.sleep(0.1) # пока она ждёт — занимаемся своим
|
||||||
|
print(" главное: что-то делаю, пока фоновая ждёт")
|
||||||
|
await task
|
||||||
|
print(f"время: {time.perf_counter() - t0:.2f} c (задачи шли вместе)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: блокирующий сон убивает event loop.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab06_asyncio/examples/02_blocking_mistake_demo.py
|
||||||
|
|
||||||
|
time.sleep (в отличие от asyncio.sleep) НЕ уступает управление:
|
||||||
|
все корутины встают в очередь. Классическая ошибка в async-коде.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
async def worker_async(name: str, delay: float) -> str:
|
||||||
|
await asyncio.sleep(delay) # уступает: другие работают
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
async def worker_blocking(name: str, delay: float) -> str:
|
||||||
|
time.sleep(delay) # НЕ уступает: парализует loop
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
async def run_all(worker, delay: float) -> float:
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
await asyncio.gather(*(worker(f"w{i}", delay) for i in range(5)))
|
||||||
|
return time.perf_counter() - t0
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
t = await run_all(worker_async, 0.2)
|
||||||
|
print(f"asyncio.sleep: 5 задач за {t:.2f} c (идеал ≈ 0.2 c)")
|
||||||
|
|
||||||
|
t = await run_all(worker_blocking, 0.2)
|
||||||
|
print(f"time.sleep: 5 задач за {t:.2f} c (последовательные 1.0 c!)")
|
||||||
|
print("\nВывод: в корутине можно вызывать только неблокирующие версии")
|
||||||
|
print("операций. time.sleep -> asyncio.sleep, requests -> aiohttp и т.д.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: asyncio.Semaphore — ограничение конкурентности.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab06_asyncio/examples/03_semaphore_demo.py
|
||||||
|
|
||||||
|
50 «запросов», сервер выдерживает 5 одновременных. Семафор не даёт
|
||||||
|
превысить лимит: время ≈ 50/5 × delay.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
async def request(i: int, delay: float) -> int:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
async def bounded(i: int, delay: float, sem: asyncio.Semaphore) -> int:
|
||||||
|
async with sem: # только 5 корутин внутри одновременно
|
||||||
|
return await request(i, delay)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
N, DELAY, LIMIT = 50, 0.05, 5
|
||||||
|
sem = asyncio.Semaphore(LIMIT)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
results = await asyncio.gather(*(bounded(i, DELAY, sem)
|
||||||
|
for i in range(N)))
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
print(f"{N} запросов, лимит {LIMIT}: {dt:.2f} c")
|
||||||
|
print(f"идеал ≈ {N / LIMIT * DELAY:.2f} c")
|
||||||
|
print(f"результаты упорядочены: {results == list(range(N))}")
|
||||||
|
print("\nБез семафора было бы ≈ 0.05 c (все сразу) — но так сервер ляжет.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 6: асинхронный код (asyncio).
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab06.py).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab06_asyncio/solution.py
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import make_table
|
||||||
|
from common.tasks import get_variant
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_async(item: int, delay: float):
|
||||||
|
"""Корутина: имитация I/O-запроса.
|
||||||
|
|
||||||
|
await asyncio.sleep(delay) — НЕ блокирует цикл событий.
|
||||||
|
Вернуть (item, item * item).
|
||||||
|
"""
|
||||||
|
# TODO: asyncio.sleep + вернуть результат
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_all_async(items: int, delay: float):
|
||||||
|
"""Все items запросов конкурентно через asyncio.gather.
|
||||||
|
|
||||||
|
Вернуть (список результатов, elapsed_seconds), упорядочен по item.
|
||||||
|
"""
|
||||||
|
# TODO: gather по всем item -> сортировка не нужна, gather сохраняет порядок
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_all_limited(items: int, delay: float, max_concurrent: int):
|
||||||
|
"""То же, но не более max_concurrent одновременно (asyncio.Semaphore).
|
||||||
|
|
||||||
|
Вернуть (список результатов, elapsed_seconds), упорядочен по item.
|
||||||
|
"""
|
||||||
|
# TODO: sem = asyncio.Semaphore(...); async with sem: await fetch_async(...)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_blocking(item: int, delay: float):
|
||||||
|
"""«Ошибка новичка»: блокирующий time.sleep внутри корутины.
|
||||||
|
|
||||||
|
time.sleep НЕ уступает управление — парализует весь event loop.
|
||||||
|
"""
|
||||||
|
time.sleep(delay)
|
||||||
|
return (item, item * item)
|
||||||
|
|
||||||
|
|
||||||
|
async def _blocking_mistake(items: int, delay: float):
|
||||||
|
"""Все запросы через gather, но с блокирующим сном. Вернуть секунды."""
|
||||||
|
# TODO: gather по _fetch_blocking, вернуть время
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def measure_blocking_mistake(items: int, delay: float) -> float:
|
||||||
|
"""Обёртка для тестов: asyncio.run + return секунд."""
|
||||||
|
# TODO: return asyncio.run(_blocking_mistake(items, delay))
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
items, delay = v["io"]["items"], v["io"]["delay"]
|
||||||
|
print(f"Вариант {v['variant']}: I/O-нагрузка {items} запросов по {delay} c\n")
|
||||||
|
|
||||||
|
# --- 1. Async: все запросы сразу ---------------------------------------
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
results, t_async = asyncio.run(fetch_all_async(items, delay))
|
||||||
|
print(f"async, без лимита: {t_async:.3f} c "
|
||||||
|
f"(упорядочено: {[r[0] for r in results] == list(range(items))})")
|
||||||
|
|
||||||
|
# --- 2. Async с лимитом --------------------------------------------------
|
||||||
|
for mc in (4, 8):
|
||||||
|
res, t_lim = asyncio.run(fetch_all_limited(items, delay, mc))
|
||||||
|
print(f"async, лимит {mc}: {t_lim:.3f} c "
|
||||||
|
f"(идеал ≈ {items / mc * delay:.2f} c)")
|
||||||
|
|
||||||
|
# --- 3. Ошибка: time.sleep в корутине -----------------------------------
|
||||||
|
t_block = measure_blocking_mistake(items, delay)
|
||||||
|
print(f"async + time.sleep (ошибка!): {t_block:.3f} c "
|
||||||
|
f"≈ последовательные {items * delay:.2f} c\n")
|
||||||
|
|
||||||
|
# --- 4. Сравнение с потоками (лаба 2) ------------------------------------
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from common.tasks import io_fetch
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||||
|
list(pool.map(io_fetch, range(items), [delay] * items))
|
||||||
|
t_threads = time.perf_counter() - t0
|
||||||
|
|
||||||
|
rows = [("async без лимита", t_async),
|
||||||
|
("async, лимит 8", t_lim),
|
||||||
|
("потоки, p=8", t_threads),
|
||||||
|
("последовательно (ошибка time.sleep)", t_block)]
|
||||||
|
print(make_table(rows, title="Сравнение подходов к I/O"))
|
||||||
|
|
||||||
|
# TODO: в отчёт — таблицу, объяснение разницы потоков и asyncio,
|
||||||
|
# ответ на вопрос «почему time.sleep убил конкурентность».
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Лабораторная работа 6. Асинхронный код (asyncio)
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары. **Зависимости:** лабы 1–2.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Освоить кооперативную многозадачность: `async/await`, event loop, `gather`,
|
||||||
|
ограничение конкурентности семафором. Понять, чем asyncio отличается от
|
||||||
|
потоков и когда выбирать её.
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Модель: один поток, но тысячи задач
|
||||||
|
|
||||||
|
Asyncio — **однопоточная** кооперативная многозадачность. Есть один цикл
|
||||||
|
событий (event loop) и множество корутин. Корутина выполняется, пока сама
|
||||||
|
не уступит управление через `await` — это ключевое отличие от потоков, где
|
||||||
|
ОС переключает задачи принудительно в любой момент (вытесняющая
|
||||||
|
многозадачность).
|
||||||
|
|
||||||
|
Следствия:
|
||||||
|
- нет гонок на `counter += 1` между await-ами: код между await-ами
|
||||||
|
исполняется атомарно (переключение — только на await);
|
||||||
|
- зато «жадная» корутина без await блокирует всё;
|
||||||
|
- одна корутина может ожидать тысячи сокетов — накладные расходы на задачу
|
||||||
|
в разы меньше, чем на поток.
|
||||||
|
|
||||||
|
### Ключевые слова и функции
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def fetch(i): # корутина — «ленивая» функция
|
||||||
|
await asyncio.sleep(0.05) # уступает управление на время ожидания
|
||||||
|
return i
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
results = await asyncio.gather( # запустить все и ждать все
|
||||||
|
fetch(1), fetch(2), fetch(3))
|
||||||
|
task = asyncio.create_task(fetch(4)) # задача: стартовала сейчас
|
||||||
|
r = await task # ...а результат ждём тут
|
||||||
|
```
|
||||||
|
|
||||||
|
`asyncio.run(main())` — вход в async-мир из обычного кода (один раз на
|
||||||
|
программу). `await` можно ставить только внутри `async def`.
|
||||||
|
|
||||||
|
### gather и семафор
|
||||||
|
|
||||||
|
`gather` запускает все корутины конкурентно. Если задач тысячи, а «сервер»
|
||||||
|
выдерживает 100 соединений — используем `asyncio.Semaphore`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
sem = asyncio.Semaphore(100)
|
||||||
|
|
||||||
|
async def limited(i):
|
||||||
|
async with sem:
|
||||||
|
return await fetch(i)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Потоки или asyncio?
|
||||||
|
|
||||||
|
| | Потоки | asyncio |
|
||||||
|
|---|---|---|
|
||||||
|
| Переключение | вытесняющее (ОС) | кооперативное (await) |
|
||||||
|
| Масштаб задач | десятки–сотни | тысячи и больше |
|
||||||
|
| Гонки данных | да | только между await-ами |
|
||||||
|
| CPU-код | бесполезен (GIL) | блокирует всё |
|
||||||
|
| Блокирующие библиотеки | подходят | блокируют цикл (!) |
|
||||||
|
|
||||||
|
Критерий: много I/O + нужно много одновременных задач → asyncio.
|
||||||
|
CPU-работа → процессы (лаба 4). Смешанная → комбинировать.
|
||||||
|
|
||||||
|
### Чем опасна блокирующая функция в async-коде
|
||||||
|
|
||||||
|
`time.sleep(0.05)` (не `asyncio.sleep`) внутри корутины останавливает ВЕСЬ
|
||||||
|
цикл событий: ни одна другая корутина не выполняется. В работе это
|
||||||
|
демонстрируется явно.
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
Заполните `solution.py` (все функции `async`, кроме `main`):
|
||||||
|
|
||||||
|
1. **`fetch_async(item, delay)`** — корутина: `await asyncio.sleep(delay)`,
|
||||||
|
вернуть `(item, item * item)`.
|
||||||
|
2. **`fetch_all_async(items, delay)`** — конкурентно все items запросов
|
||||||
|
через `asyncio.gather`. Вернуть `(результаты, сек)`, упорядочены по item.
|
||||||
|
3. **`fetch_all_limited(items, delay, max_concurrent)`** — то же, но
|
||||||
|
не более max_concurrent одновременно (`asyncio.Semaphore`).
|
||||||
|
4. **`measure_blocking_mistake(items, delay)`** — «ошибка новичка»:
|
||||||
|
корутина использует `time.sleep(delay)` вместо `asyncio.sleep`.
|
||||||
|
Вернуть время выполнения: оно окажется ≈ items × delay — блокирующий
|
||||||
|
сон парализует цикл событий.
|
||||||
|
5. **`main()`** — все пункты + сравнение с потоками (лаба 2).
|
||||||
|
|
||||||
|
### Ожидаемые результаты
|
||||||
|
|
||||||
|
- `fetch_all_async(96, 0.05)` ≈ 0.05–0.1 c — все запросы «одновременно».
|
||||||
|
- `fetch_all_limited(96, 0.05, 8)` ≈ 0.6 c.
|
||||||
|
- `measure_blocking_mistake(96, 0.05)` ≈ 4.8 c — блокирующий sleep убил
|
||||||
|
конкурентность.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Чем кооперативная многозадачность отличается от вытесняющей?
|
||||||
|
2. Почему между двумя `await` корутина «владеет» данными без Lock?
|
||||||
|
3. Что случится, если в корутине вызвать `time.sleep(10)`? Проверьте.
|
||||||
|
4. Когда выбрали бы потоки, а когда asyncio? Приведите по примеру.
|
||||||
|
5. Что делает `asyncio.gather` и чем отличается от последовательных `await`?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: таблица сравнения (последовательно/потоки/async/
|
||||||
|
async+лимит/ошибка с time.sleep), анализ, ответы на вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| fetch_all_async: время ≈ delay, результаты упорядочены | 2 |
|
||||||
|
| fetch_all_limited: лимит соблюдается | 2 |
|
||||||
|
| measure_blocking_mistake: продемонстрирована деградация | 2 |
|
||||||
|
| Сравнительная таблица в отчёте | 2 |
|
||||||
|
| Ответы на контрольные вопросы | 2 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- `await` вне `async def` — SyntaxError.
|
||||||
|
- Забыт `await` перед корутиной: `RuntimeWarning: never awaited`, ничего
|
||||||
|
не выполняется.
|
||||||
|
- `asyncio.run()` внутри работающего цикла (Jupyter) — RuntimeError.
|
||||||
|
- `time.sleep` в корутине — «секретный» последовательный режим (это п.4
|
||||||
|
работы, тут это фича).
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 7 (итоговая): одна задача — четыре подхода.
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab07.py, запускаются через подпроцесс).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab07_comparison/solution.py
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.benchmark import amdahl, make_table, save_speedup_plot, time_call
|
||||||
|
from common.tasks import TASKS, dispatch_kernel, get_variant, io_fetch
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= CPU-сценарий ====================================
|
||||||
|
|
||||||
|
def cpu_sequential(task_name: str, params: dict):
|
||||||
|
"""Последовательно (лаба 1). Вернуть (checksum, сек)."""
|
||||||
|
task = TASKS[task_name]
|
||||||
|
# TODO: build -> замер времени -> split(data, 1) -> kernel -> combine -> checksum
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_threads(task_name: str, params: dict, p: int):
|
||||||
|
"""На p потоках (лаба 3): ожидание S ≈ 1 из-за GIL. Вернуть (checksum, сек)."""
|
||||||
|
# TODO: как в лабе 3 — ThreadPoolExecutor, kernel напрямую в воркере
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def cpu_processes(task_name: str, params: dict, p: int):
|
||||||
|
"""На p процессах (лаба 4): ожидание S ≈ ядра. Вернуть (checksum, сек)."""
|
||||||
|
# TODO: как в лабе 4 — ctx.Pool + dispatch_kernel
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= I/O-сценарий ====================================
|
||||||
|
|
||||||
|
def io_sequential(items: int, delay: float):
|
||||||
|
"""Последовательно (лаба 1). Вернуть (список, сек)."""
|
||||||
|
# TODO: [io_fetch(i, delay) for i in range(items)] с замером
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def io_threads(items: int, delay: float, p: int):
|
||||||
|
"""На p потоках (лаба 2). Вернуть (список, сек)."""
|
||||||
|
# TODO: ThreadPoolExecutor + pool.map(io_fetch, ..., [delay]*items)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
async def _io_async_coro(items: int, delay: float):
|
||||||
|
"""Корутина (лаба 6): gather + asyncio.sleep. Вернуть (список, сек)."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def io_async(items: int, delay: float):
|
||||||
|
"""Обёртка asyncio.run. Вернуть (список, сек)."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def io_async_limited(items: int, delay: float, max_concurrent: int = 8):
|
||||||
|
"""Async с семафором (лаба 6). Вернуть (список, сек)."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= main ============================================
|
||||||
|
|
||||||
|
def _smoke():
|
||||||
|
"""Smoke-режим автотестов: быстрый прогон всех подходов, JSON-отчёт.
|
||||||
|
(не удаляйте: тесты запускают solution.py подпроцессом с LAB_SMOKE=1)"""
|
||||||
|
from common.tasks import smoke_params, run_sequential
|
||||||
|
name, sp = "integrate", smoke_params("integrate")
|
||||||
|
exp, _ = run_sequential(name, sp, parts=1)
|
||||||
|
|
||||||
|
cs1, _ = cpu_threads(name, sp, 2)
|
||||||
|
cs2, _ = cpu_processes(name, sp, 2)
|
||||||
|
ok_cpu = (cs1 == exp or abs(cs1 - exp) < 1e-6) and \
|
||||||
|
(cs2 == exp or abs(cs2 - exp) < 1e-6)
|
||||||
|
|
||||||
|
res, t_seq = io_sequential(8, 0.0)
|
||||||
|
res2, t_thr = io_threads(8, 0.02, 4)
|
||||||
|
res3, t_asy = io_async(8, 0.02)
|
||||||
|
ok_io = len(res) == 8 and len(res2) == 8 and len(res3) == 8
|
||||||
|
|
||||||
|
print("LAB_SMOKE_JSON:" + json.dumps({
|
||||||
|
"ok": True, "cpu_ok": bool(ok_cpu), "io_ok": bool(ok_io),
|
||||||
|
"p": 2, "io_times": {"seq": t_seq, "threads": t_thr, "async": t_asy}}))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if os.environ.get("LAB_SMOKE") == "1":
|
||||||
|
_smoke()
|
||||||
|
return
|
||||||
|
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
items, delay = v["io"]["items"], v["io"]["delay"]
|
||||||
|
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||||
|
print(f"I/O: {items} запросов по {delay} c\n")
|
||||||
|
|
||||||
|
# --- CPU-сценарий -------------------------------------------------------
|
||||||
|
print("=== CPU-сценарий ===")
|
||||||
|
cs0, t_seq = cpu_sequential(v["task_name"], v["cpu_params"])
|
||||||
|
print(f"последовательно: {t_seq:.3f} c")
|
||||||
|
cpu_results = {"потоки": [], "процессы": []}
|
||||||
|
for p in v["p_list"]:
|
||||||
|
_, (c1, t_thr) = time_call(cpu_threads,
|
||||||
|
(v["task_name"], v["cpu_params"], p), repeats=2)
|
||||||
|
_, (c2, t_proc) = time_call(cpu_processes,
|
||||||
|
(v["task_name"], v["cpu_params"], p), repeats=2)
|
||||||
|
assert (c1 == cs0 or abs(c1 - cs0) < 1e-6) and \
|
||||||
|
(c2 == cs0 or abs(c2 - cs0) < 1e-6), f"checksum не совпал при p={p}"
|
||||||
|
cpu_results["потоки"].append((p, t_thr))
|
||||||
|
cpu_results["процессы"].append((p, t_proc))
|
||||||
|
print(f"p={p}: потоки {t_thr:.3f} c (S={t_seq/t_thr:.2f}) | "
|
||||||
|
f"процессы {t_proc:.3f} c (S={t_seq/t_proc:.2f})")
|
||||||
|
|
||||||
|
# --- I/O-сценарий ---------------------------------------------------------
|
||||||
|
print("\n=== I/O-сценарий ===")
|
||||||
|
_, t_ioseq = io_sequential(items, delay)
|
||||||
|
_, t_iothr = io_threads(items, delay, 8)
|
||||||
|
_, t_ioasy = io_async(items, delay)
|
||||||
|
_, t_ioasy_lim = io_async_limited(items, delay, 8)
|
||||||
|
io_rows = [("последовательно", t_ioseq),
|
||||||
|
("потоки, p=8", t_iothr),
|
||||||
|
("async", t_ioasy),
|
||||||
|
("async, лимит 8", t_ioasy_lim)]
|
||||||
|
print(make_table(io_rows, title="I/O-сценарий"))
|
||||||
|
|
||||||
|
# --- График CPU ------------------------------------------------------------
|
||||||
|
save_speedup_plot(cpu_results,
|
||||||
|
f"CPU: ускорение ({v['describe']})",
|
||||||
|
os.path.join(os.path.dirname(__file__), "speedup_cpu.png"))
|
||||||
|
|
||||||
|
# --- Оценка s по Амдалу -----------------------------------------------------
|
||||||
|
p_max = max(v["p_list"])
|
||||||
|
s_meas = t_seq / cpu_results["процессы"][-1][1]
|
||||||
|
best_s, best_diff = None, float("inf")
|
||||||
|
for i in range(1, 200): # перебираем s = 0.005 .. 0.995
|
||||||
|
s = i / 200
|
||||||
|
diff = abs(amdahl(s, p_max) - s_meas)
|
||||||
|
if diff < best_diff:
|
||||||
|
best_diff, best_s = diff, s
|
||||||
|
print(f"\nИзмеренное S({p_max}) = {s_meas:.2f}; "
|
||||||
|
f"по закону Амдала это соответствует s ≈ {best_s:.3f} "
|
||||||
|
f"(доля последовательного кода с накладными расходами)")
|
||||||
|
|
||||||
|
# TODO: в отчёт — обе таблицы, график, объяснение s, развёрнутые выводы
|
||||||
|
# по всему курсу (это итоговая работа).
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Лабораторная работа 7. Сравнительный анализ: одна задача — четыре подхода
|
||||||
|
|
||||||
|
**Время на работу:** 2 пары. **Зависимости:** лабы 1–6. Итоговая работа.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Свести весь курс в одну таблицу: ваша CPU-задача и ваша I/O-нагрузка,
|
||||||
|
выполненные последовательно, потоками, процессами и асинхронно. Объяснить
|
||||||
|
каждую цифру. Это работа для защиты: по ней проверяется понимание всего
|
||||||
|
курса.
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Ускорение и эффективность
|
||||||
|
|
||||||
|
- Ускорение: **S(p) = T(1) / T(p)** — во сколько раз быстрее на p воркерах.
|
||||||
|
- Эффективность: **E(p) = S(p) / p** — насколько эффективно используется
|
||||||
|
каждый воркер. E = 1 — идеал, E = 0.5 — половина мощности простаивает.
|
||||||
|
|
||||||
|
### Закон Амдала
|
||||||
|
|
||||||
|
Если доля последовательного кода задачи равна s (0 ≤ s ≤ 1), то ускорение
|
||||||
|
ограничено:
|
||||||
|
|
||||||
|
```
|
||||||
|
S(p) ≤ 1 / (s + (1 − s) / p)
|
||||||
|
```
|
||||||
|
|
||||||
|
При s = 0.1 и p → ∞: S ≤ 10. Даже бесконечное число воркеров не даст
|
||||||
|
больше 10×. Отсюда главный вывод курса: **сначала сокращайте
|
||||||
|
последовательную часть и накладные расходы, потом наращивайте p.**
|
||||||
|
|
||||||
|
В `common.benchmark` есть `amdahl(s, p)` — сравните свои S(p) с теорией:
|
||||||
|
зная измеренное S(p), можно оценить эффективную долю последовательной части
|
||||||
|
вашего эксперимента (создание пулов, pickle, GIL-переключения — всё это
|
||||||
|
«последовательная» часть).
|
||||||
|
|
||||||
|
### Матрица выбора модели
|
||||||
|
|
||||||
|
| Задача | Потоки | Процессы | asyncio |
|
||||||
|
|---|---|---|---|
|
||||||
|
| CPU-bound (чистый Python) | ✗ (GIL) | ✓ | ✗ (блокирует loop) |
|
||||||
|
| CPU-bound в C-коде (hashlib, numpy) | ✓ | ✓ | ✗ |
|
||||||
|
| I/O-bound, десятки задач | ✓ | возможно (дорого) | ✓ |
|
||||||
|
| I/O-bound, тысячи задач | дорого | ✗ | ✓✓ |
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
Заполните `solution.py` — оба сценария из ваших лабораторных:
|
||||||
|
|
||||||
|
**CPU-сценарий** (задача из варианта):
|
||||||
|
1. `cpu_sequential(task_name, params)` — как в лабе 1;
|
||||||
|
2. `cpu_threads(task_name, params, p)` — как в лабе 3 (S ≈ 1!);
|
||||||
|
3. `cpu_processes(task_name, params, p)` — как в лабе 4 (S ≈ ядра).
|
||||||
|
|
||||||
|
**I/O-сценарий** (items × delay из варианта):
|
||||||
|
4. `io_sequential(items, delay)` — как в лабе 1;
|
||||||
|
5. `io_threads(items, delay, p)` — как в лабе 2;
|
||||||
|
6. `io_async(items, delay)` — как в лабе 6;
|
||||||
|
7. `io_async_limited(items, delay, max_concurrent)` — с семафором 8.
|
||||||
|
|
||||||
|
**`main()`** — таблицы для обоих сценариев с T(p), S(p), E(p); два графика
|
||||||
|
ускорения; оценка доли s по закону Амдала для процессов.
|
||||||
|
|
||||||
|
### Ожидаемые результаты (сверьтесь)
|
||||||
|
|
||||||
|
CPU: потоки S ≈ 1; процессы S(p) ≈ до числа физических ядер.
|
||||||
|
I/O: последовательный — база; потоки и async — S ≈ p; async без лимита —
|
||||||
|
минимальное время (нет расходов на потоки); processes для I/O — работает,
|
||||||
|
но с заметными накладными расходами.
|
||||||
|
|
||||||
|
### Что дополнительно оценить
|
||||||
|
|
||||||
|
- Разница T(1) у процессов против последовательного — цена старта процессов
|
||||||
|
и pickle.
|
||||||
|
- По S(p_max) процессов найдите s через `amdahl` (подберите s так, чтобы
|
||||||
|
теория совпала с практикой) — укажите его в отчёте.
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Почему потоки не ускорили CPU-задачу, но ускорили I/O?
|
||||||
|
2. Почему asyncio на I/O обгоняет потоки? Откуда выигрыш?
|
||||||
|
3. Какая доля последовательного кода получилась у процессов по Амдалу?
|
||||||
|
Что в неё входит?
|
||||||
|
4. Какую модель вы выбрали бы для веб-краулера на 10 000 страниц?
|
||||||
|
Для рендера видео? Обоснуйте.
|
||||||
|
5. Что в ваших замерах не совпало с теорией и почему?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: две таблицы (CPU и I/O) с T/S/E, два графика,
|
||||||
|
оценка s по Амдалу, развёрнутые выводы (это итоговая работа — выводы
|
||||||
|
должны покрывать весь курс), ответы на вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| Все 7 функций работают, checksum корректен | 3 |
|
||||||
|
| Таблицы и графики для обоих сценариев | 3 |
|
||||||
|
| Оценка s по закону Амдала с объяснением | 2 |
|
||||||
|
| Выводы и ответы на вопросы (защита) | 4 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- Сравнение T(p) разных сценариев при разном объёме работы — объём
|
||||||
|
фиксирован внутри сценария.
|
||||||
|
- Забыт smoke-режим `LAB_SMOKE` (автотесты запускают solution.py
|
||||||
|
подпроцессом — не удаляйте блок в main()).
|
||||||
|
- Замеры в один запуск без повторов: `time_call(..., repeats=3)`.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: привет, MPI — ранги, размер мира, точка-точка.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
mpiexec -n 4 python lab08_mpi/examples/01_hello_mpi.py
|
||||||
|
|
||||||
|
Каждый ранг — отдельный процесс с копией программы. Ранги обмениваются
|
||||||
|
сообщениями: ранг 0 рассылает своё имя всем, каждый отвечает.
|
||||||
|
Без mpiexec (обычный python) программа работает как единственный ранг.
|
||||||
|
"""
|
||||||
|
from mpi4py import MPI
|
||||||
|
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
print(f"[до обмена] ранг {rank} из {size}, узел {MPI.Get_processor_name()}")
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
# ранг 0 рассылает задание всем остальным (bcast: всем одно и то же)
|
||||||
|
greeting = f"привет от координатора (ранг 0), всего рангов {size}"
|
||||||
|
else:
|
||||||
|
greeting = None
|
||||||
|
|
||||||
|
greeting = comm.bcast(greeting, root=0) # коллективная: вызывают ВСЕ
|
||||||
|
print(f"ранг {rank}: получил «{greeting}»")
|
||||||
|
|
||||||
|
# точка-точка: каждый ранг (кроме 0) отправляет 0 свой номер
|
||||||
|
if rank != 0:
|
||||||
|
comm.send(rank, dest=0, tag=11)
|
||||||
|
print(f"ранг {rank}: отправил свой номер координатору")
|
||||||
|
else:
|
||||||
|
received = [comm.recv(source=r, tag=11) for r in range(1, size)]
|
||||||
|
print(f"ранг 0: получил номера от рангов {received}")
|
||||||
|
|
||||||
|
if size == 1:
|
||||||
|
print("\nВы запустили без mpiexec — работает один ранг.")
|
||||||
|
print("Попробуйте: mpiexec -n 4 python lab08_mpi/examples/01_hello_mpi.py")
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: ping-pong и замер стоимости обмена между рангами.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
mpiexec -n 2 python lab08_mpi/examples/02_pingpong_mpi.py
|
||||||
|
|
||||||
|
Ранги перекидывают список чисел туда-обратно. Так измеряют латентность и
|
||||||
|
пропускную способность канала между узлами.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mpi4py import MPI
|
||||||
|
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
if size < 2:
|
||||||
|
print("Нужны минимум 2 ранга: mpiexec -n 2 python lab08_mpi/examples/02_pingpong_mpi.py")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
ROUNDS = 20
|
||||||
|
for n_bytes in (1_000, 100_000, 1_000_000):
|
||||||
|
data = list(range(n_bytes // 8)) # ~n_bytes байт целых чисел
|
||||||
|
comm.barrier() # синхронный старт замера
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
if rank == 0:
|
||||||
|
for _ in range(ROUNDS):
|
||||||
|
comm.send(data, dest=1, tag=0)
|
||||||
|
comm.recv(source=1, tag=0)
|
||||||
|
elif rank == 1:
|
||||||
|
for _ in range(ROUNDS):
|
||||||
|
comm.recv(source=0, tag=0)
|
||||||
|
comm.send(data, dest=0, tag=0)
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
per_round = dt / ROUNDS
|
||||||
|
print(f"{n_bytes:>9,} байт: {ROUNDS} раундов за {dt:.3f} c "
|
||||||
|
f"=> {per_round * 1000:.2f} мс/раунд (туда-обратно)".replace(",", " "))
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
print("\nНа одной машине это IPC (быстро), между узлами — сеть.")
|
||||||
|
print("Эта цена обмена и есть накладные расходы распределённых вычислений.")
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: коллективные операции scatter / gather / reduce / bcast.
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
mpiexec -n 4 python lab08_mpi/examples/03_collectives_mpi.py
|
||||||
|
|
||||||
|
Показывает все четыре коллективные операции на маленьком примере.
|
||||||
|
"""
|
||||||
|
from mpi4py import MPI
|
||||||
|
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
# --- bcast: один -> всем ------------------------------------------------
|
||||||
|
config = {"task": "summa_kvadratov", "n": 100} if rank == 0 else None
|
||||||
|
config = comm.bcast(config, root=0)
|
||||||
|
print(f"ранг {rank}: config = {config}")
|
||||||
|
|
||||||
|
# --- scatter: СПИСОК КУСКОВ (ровно size штук), ранг i получает i-й --------
|
||||||
|
if rank == 0:
|
||||||
|
# каждый кусок — свой список; scatter ждёт список из size кусков!
|
||||||
|
numbers = [[i * 10 + j for j in range(10)] for i in range(size)]
|
||||||
|
else:
|
||||||
|
numbers = None
|
||||||
|
my_chunk = comm.scatter(numbers, root=0) # ранг i получает свой кусок
|
||||||
|
print(f"ранг {rank}: мой кусок {my_chunk[0]}..{my_chunk[-1]} ({len(my_chunk)} шт.)")
|
||||||
|
|
||||||
|
# --- каждый ранг считает СВОЮ часть --------------------------------------
|
||||||
|
my_sum = sum(x * x for x in my_chunk)
|
||||||
|
print(f"ранг {rank}: сумма квадратов моего куска = {my_sum}")
|
||||||
|
|
||||||
|
# --- gather: все части -> список на ранге 0 ------------------------------
|
||||||
|
partial_sums = comm.gather(my_sum, root=0)
|
||||||
|
if rank == 0:
|
||||||
|
print(f"ранг 0: частичные суммы = {partial_sums}")
|
||||||
|
print(f"ранг 0: итого через sum(gather) = {sum(partial_sums)}")
|
||||||
|
|
||||||
|
# --- reduce: то же самое, но сразу со свёрткой (проще и быстрее) ----------
|
||||||
|
total = comm.reduce(my_sum, op=MPI.SUM, root=0)
|
||||||
|
if rank == 0:
|
||||||
|
print(f"ранг 0: итого через reduce(MPI.SUM) = {total}")
|
||||||
|
print("\ngather+sum и reduce(SUM) дали одно и то же — reduce короче.")
|
||||||
|
print("В задании mpi_pi вы используете reduce для суммы попаданий.")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Демо: π методом Монте-Карло в MPI (reduce).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
mpiexec -n 4 python lab08_mpi/examples/04_pi_reduce_mpi.py
|
||||||
|
|
||||||
|
Каждый ранг бросает свою порцию «точек» со СВОИМ зерном (rank-зависимым),
|
||||||
|
reduce(SUM) сворачивает попадания. Число рангов не меняет формулу —
|
||||||
|
меняется только распределение бросков.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mpi4py import MPI
|
||||||
|
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
N_PER_RANK = 500_000
|
||||||
|
|
||||||
|
|
||||||
|
def count_inside(n: int, seed: int) -> int:
|
||||||
|
rng = random.Random(seed) # зерно зависит от ранга
|
||||||
|
inside = 0
|
||||||
|
for _ in range(n):
|
||||||
|
x = rng.random()
|
||||||
|
y = rng.random()
|
||||||
|
if x * x + y * y <= 1.0:
|
||||||
|
inside += 1
|
||||||
|
return inside
|
||||||
|
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
my_inside = count_inside(N_PER_RANK, seed=1000 + rank)
|
||||||
|
total_inside = comm.reduce(my_inside, op=MPI.SUM, root=0)
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
total = N_PER_RANK * size
|
||||||
|
pi = 4.0 * total_inside / total
|
||||||
|
print(f"рангов: {size}, бросков на ранг: {N_PER_RANK:,} (всего {total:,})")
|
||||||
|
print(f"π ≈ {pi:.6f} (погрешность {abs(pi - 3.141592653589793):.6f})")
|
||||||
|
print(f"время: {dt:.2f} c")
|
||||||
|
if size == 1:
|
||||||
|
print("\nПопробуйте: mpiexec -n 4 python lab08_mpi/examples/04_pi_reduce_mpi.py")
|
||||||
|
print("Каждый ранг добавляет 500k бросков: точность растёт, время — нет.")
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Лабораторная работа 8: распределённые вычисления (mpi4py).
|
||||||
|
|
||||||
|
Заполните функции ниже. ИНТЕРФЕЙСЫ МЕНЯТЬ НЕЛЬЗЯ — по ним работают
|
||||||
|
автотесты (tests/test_lab08.py; если mpi4py не установлен — тесты
|
||||||
|
пропускаются, но работа сдаётся с запуском через mpiexec).
|
||||||
|
|
||||||
|
Запуск из корня курса:
|
||||||
|
python lab08_mpi/solution.py (один ранг — отладка)
|
||||||
|
mpiexec -n 4 python lab08_mpi/solution.py (4 ранга — замеры)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from common.tasks import TASKS, get_variant
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mpi4py import MPI
|
||||||
|
HAVE_MPI = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_MPI = False
|
||||||
|
|
||||||
|
# TODO: впишите свой номер в журнале (1..20)
|
||||||
|
VARIANT_NUMBER = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _group_for_ranks(chunks, size):
|
||||||
|
"""Распределить куски по рангам round-robin: список из size групп.
|
||||||
|
|
||||||
|
scatter ожидает список ровно из size элементов, а естественных кусков
|
||||||
|
может быть другое число (например, nqueens всегда даёт N кусков) —
|
||||||
|
поэтому группируем.
|
||||||
|
"""
|
||||||
|
# TODO: groups = [[] for _ in range(size)]; groups[i % size].append(ch)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def mpi_run(task_name: str, params: dict):
|
||||||
|
"""CPU-задача на всех рангах: scatter групп -> kernel -> gather -> combine.
|
||||||
|
|
||||||
|
Схема:
|
||||||
|
ранг 0: build -> split(data, size) -> _group_for_ranks
|
||||||
|
scatter раздаёт ГРУППЫ кусков (по одной на ранг)
|
||||||
|
каждый ранг: my_partials = [task["kernel"](ch) for ch in my_group]
|
||||||
|
gather собирает списки частичных результатов
|
||||||
|
ранг 0: развернуть в плоский список -> combine ОДИН раз -> checksum
|
||||||
|
|
||||||
|
Функции пиккелить не нужно (в отличие от multiprocessing) — только данные.
|
||||||
|
|
||||||
|
Для задачи 'life': данные — (grid, steps), параллелятся ПОКОЛЕНИЯ.
|
||||||
|
Ранг 0 держит текущее поле; каждый шаг: scatter полос -> kernel ->
|
||||||
|
gather -> combine -> новое поле. Число шагов раздаётся через bcast.
|
||||||
|
"""
|
||||||
|
if not HAVE_MPI:
|
||||||
|
raise RuntimeError("mpi4py не установлен (см. методичку, раздел Установка)")
|
||||||
|
task = TASKS[task_name]
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
if task_name == "life":
|
||||||
|
# TODO: цикл по поколениям (см. схему выше); в конце bcast поля
|
||||||
|
# с ранга 0 всем, чтобы checksum был у всех рангов
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# TODO: общий случай по схеме выше; вернуть checksum на ранге 0 (None на других)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def mpi_pi(N: int) -> float:
|
||||||
|
"""π методом Монте-Карло на всех рангах (reduce).
|
||||||
|
|
||||||
|
Каждый ранг бросает N//size точек с зерном random.Random(100 + rank).
|
||||||
|
reduce(MPI.SUM) сворачивает попадания на ранге 0.
|
||||||
|
Вернуть π (на ранге 0 — вычисленное значение, на остальных 0.0).
|
||||||
|
"""
|
||||||
|
# TODO: my_n = N // size; цикл бросков; comm.reduce(inside, op=MPI.SUM, root=0)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not HAVE_MPI:
|
||||||
|
print("mpi4py не установлен. Инструкция — в методичке, раздел «Установка».")
|
||||||
|
print("Кратко (Windows): MS-MPI runtime + pip install mpi4py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
v = get_variant(VARIANT_NUMBER)
|
||||||
|
comm = MPI.COMM_WORLD
|
||||||
|
rank = comm.Get_rank()
|
||||||
|
size = comm.Get_size()
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
print(f"Вариант {v['variant']}: {v['describe']}")
|
||||||
|
print(f"Запущено рангов: {size}\n")
|
||||||
|
|
||||||
|
# --- pi через reduce ------------------------------------------------------
|
||||||
|
pi = mpi_pi(1_000_000)
|
||||||
|
if rank == 0:
|
||||||
|
print(f"π ≈ {pi:.6f} (Монте-Карло, 1 000 000 бросков суммарно)")
|
||||||
|
|
||||||
|
# --- основная задача --------------------------------------------------------
|
||||||
|
comm.barrier()
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
checksum = mpi_run(v["task_name"], v["cpu_params"])
|
||||||
|
dt = time.perf_counter() - t0
|
||||||
|
|
||||||
|
if rank == 0:
|
||||||
|
from common.tasks import run_sequential
|
||||||
|
ref, _ = run_sequential(v["task_name"], v["cpu_params"], parts=1)
|
||||||
|
ok = checksum == ref or (isinstance(checksum, float) and
|
||||||
|
abs(checksum - ref) < 1e-6 * max(1, abs(ref)))
|
||||||
|
print(f"задача: {dt:.3f} c, checksum совпадает с последовательным: {ok}")
|
||||||
|
print("\nЗапустите при разном числе рангов для таблицы T(p):")
|
||||||
|
print(" mpiexec -n 1 python lab08_mpi/solution.py")
|
||||||
|
print(" mpiexec -n 2 python lab08_mpi/solution.py")
|
||||||
|
print(" mpiexec -n 4 python lab08_mpi/solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Лабораторная работа 8. Распределённые вычисления (MPI)
|
||||||
|
|
||||||
|
**Время на работу:** 2–3 пары. **Зависимости:** лабы 1, 4.
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Освоить модель передачи сообщений (MPI) — стандарт распределённых вычислений,
|
||||||
|
на котором работают суперкомпьютеры. Понять отличие «общей памяти»
|
||||||
|
(потоки/процессы Python) от «обмена сообщениями» между независимыми узлами.
|
||||||
|
|
||||||
|
## Теория
|
||||||
|
|
||||||
|
### Модель MPI
|
||||||
|
|
||||||
|
MPI (Message Passing Interface) — набор программ на нескольких **узлах**
|
||||||
|
(машинах или процессах), обменивающихся сообщениями. Нет общей памяти: любые
|
||||||
|
данные передаются явно, функциями отправки/приёма. Программа запускается
|
||||||
|
одновременно на N «рангах» (процессах):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
mpiexec -n 4 python script.py # 4 копии script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждая копия узнаёт, кто она:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mpi4py import MPI
|
||||||
|
comm = MPI.COMM_WORLD # «мир» — все ранги
|
||||||
|
rank = comm.Get_rank() # мой номер: 0..size-1
|
||||||
|
size = comm.Get_size() # всего рангов
|
||||||
|
```
|
||||||
|
|
||||||
|
Ранг 0 обычно координатор: он делит работу и собирает результат.
|
||||||
|
|
||||||
|
### Операции: «точка-точка» и коллективные
|
||||||
|
|
||||||
|
**Точка-точка** (ранг ↔ ранг):
|
||||||
|
|
||||||
|
```python
|
||||||
|
comm.send(data, dest=1, tag=0) # блокирующая отправка
|
||||||
|
data = comm.recv(source=0, tag=0) # блокирующий приём
|
||||||
|
```
|
||||||
|
|
||||||
|
**Коллективные** (все ранги сразу — проще и обычно быстрее):
|
||||||
|
|
||||||
|
| Операция | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `bcast` | один раздает всем (broadcast) |
|
||||||
|
| `scatter` | список режется на части: ранг i получает i-ю часть |
|
||||||
|
| `gather` | части собираются в список на одном ранге |
|
||||||
|
| `reduce` | части сворачиваются операцией (sum/max/…) на одном ранге |
|
||||||
|
| `barrier` | все ждут, пока все дойдут до этой точки |
|
||||||
|
|
||||||
|
### Схема параллельной задачи в MPI
|
||||||
|
|
||||||
|
```
|
||||||
|
ранг 0: build -> split -> scatter ─┐
|
||||||
|
ранги 1..N: получают кусок │-> kernel -> gather -> combine -> checksum
|
||||||
|
ранг 0: собирает ┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Это ровно ваша схема split→kernel→combine из лаб 1–4, только обмен данными —
|
||||||
|
через сеть/IPC, а не общую память.
|
||||||
|
|
||||||
|
### Установка (Windows)
|
||||||
|
|
||||||
|
1. MS-MPI: скачать и установить **msmpisetup.exe** и **msmpires.msi**
|
||||||
|
(https://www.microsoft.com/en-us/download/details.aspx?id=105289).
|
||||||
|
2. `pip install mpi4py`
|
||||||
|
3. Проверка: `mpiexec -n 2 python -c "from mpi4py import MPI; print(MPI.COMM_WORLD.Get_rank())"`
|
||||||
|
|
||||||
|
На macOS/Linux: `brew install open-mpi` / `apt install libopenmpi-dev`,
|
||||||
|
затем `pip install mpi4py`, запуск `mpiexec` (или `mpirun`).
|
||||||
|
|
||||||
|
Важно: запускать `mpiexec -n 4` — тогда `solution.py` выполнится 4 раза
|
||||||
|
параллельно. Запуск без mpiexec допустим: программа работает как один ранг
|
||||||
|
(полезно для отладки и автотестов).
|
||||||
|
|
||||||
|
## Задание
|
||||||
|
|
||||||
|
Заполните `solution.py`:
|
||||||
|
|
||||||
|
1. **`mpi_run(task_name, params)`** — параллельное выполнение вашей CPU-задачи:
|
||||||
|
ранг 0 строит данные и делит на `size` кусков, `scatter` раздаёт,
|
||||||
|
каждый ранг считает `kernel` (используя `dispatch_kernel` не нужно —
|
||||||
|
в MPI функции не пиккелятся, только данные), `gather` собирает,
|
||||||
|
ранг 0 возвращает checksum. Для задачи «Жизнь» — цикл по поколениям
|
||||||
|
(см. подсказку в заготовке).
|
||||||
|
2. **`mpi_pi(N)`** — учебный пример: π методом Монте-Карло на всех рангах,
|
||||||
|
сборка через `reduce` (сумма попаданий). Каждый ранг использует
|
||||||
|
`random.Random(100 + rank)` — свои броски.
|
||||||
|
3. **`main()`** — checksum и время `mpi_run` на текущем числе рангов
|
||||||
|
(`size`), печать с ранга 0. Запустите при `-n 1, 2, 4` — получите T(p).
|
||||||
|
|
||||||
|
### Ожидаемые результаты
|
||||||
|
|
||||||
|
- checksum совпадает с последовательным запуском (лаба 1).
|
||||||
|
- T(p) на одном компьютере ≈ результатам multiprocessing из лабы 4
|
||||||
|
(плюс небольшие расходы на обмен).
|
||||||
|
- На настоящих нескольких машинах код не меняется — меняется только
|
||||||
|
запуск (это главный смысл MPI).
|
||||||
|
|
||||||
|
### Измерения для отчёта
|
||||||
|
|
||||||
|
```bat
|
||||||
|
mpiexec -n 1 python lab08_mpi\solution.py
|
||||||
|
mpiexec -n 2 python lab08_mpi\solution.py
|
||||||
|
mpiexec -n 4 python lab08_mpi\solution.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Контрольные вопросы
|
||||||
|
|
||||||
|
1. Чем модель передачи сообщений отличается от модели общей памяти?
|
||||||
|
2. Что делает `scatter` и чем она отличается от `bcast`?
|
||||||
|
3. Почему в MPI функции не нужно пиккелить, а в multiprocessing — нужно?
|
||||||
|
4. Что такое ранг? Как ранг 0 узнаёт, сколько всего рангов?
|
||||||
|
5. Когда MPI выгоднее multiprocessing, даже на одном компьютере?
|
||||||
|
|
||||||
|
## Что сдаётся
|
||||||
|
|
||||||
|
`solution.py` + отчёт: вывод при `-n 1/2/4`, таблица T(p), сравнение с
|
||||||
|
лабой 4, ответы на вопросы.
|
||||||
|
|
||||||
|
## Критерии оценки
|
||||||
|
|
||||||
|
| Пункт | Баллы |
|
||||||
|
|---|---|
|
||||||
|
| mpi_run: checksum корректен при -n 1 и -n 4 | 3 |
|
||||||
|
| mpi_pi через reduce | 2 |
|
||||||
|
| Замеры при разном числе рангов | 2 |
|
||||||
|
| Отчёт и ответы на вопросы | 3 |
|
||||||
|
|
||||||
|
## Типичные ошибки
|
||||||
|
|
||||||
|
- Код с веткой «если rank == 0» выполняется ВСЕМИ рангами — не заворачивайте
|
||||||
|
всю программу в `if rank == 0`.
|
||||||
|
- Разные random на разных рангах без фиксации зерна — невоспроизводимый
|
||||||
|
результат.
|
||||||
|
- Заблокированный `recv` без парной `send` — программа висит. Начинайте с
|
||||||
|
коллективных операций, они безопаснее.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Обязательные зависимости курса
|
||||||
|
numpy>=1.24
|
||||||
|
matplotlib>=3.7
|
||||||
|
pytest>=7.0
|
||||||
|
|
||||||
|
# Опционально (только для лабораторной работы 8, MPI):
|
||||||
|
# Windows: установить MS-MPI (https://www.microsoft.com/en-us/download/details.aspx?id=105289),
|
||||||
|
# затем: pip install mpi4py
|
||||||
|
# mpi4py>=3.1
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Общие фикстуры и помощники для автотестов курса.
|
||||||
|
|
||||||
|
Запуск из корня курса: python -m pytest tests/ -k lab01 -v
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
def load_solution(lab_dir: str):
|
||||||
|
"""Импортирует solution.py из папки лабораторной как отдельный модуль."""
|
||||||
|
path = ROOT / lab_dir / "solution.py"
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"не найден {path}")
|
||||||
|
spec = importlib.util.spec_from_file_location(f"solution_{lab_dir}", path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def close(a, b, rel=1e-6, abs_tol=1e-6):
|
||||||
|
"""Сравнение с допуском: для float-результатов (π, интеграл, checksum).
|
||||||
|
|
||||||
|
Принимает как значения, так и кортежи (value, seconds), которые
|
||||||
|
возвращают функции-заготовки студентов.
|
||||||
|
"""
|
||||||
|
if isinstance(a, tuple):
|
||||||
|
a = a[0]
|
||||||
|
if isinstance(b, tuple):
|
||||||
|
b = b[0]
|
||||||
|
if isinstance(a, int) and isinstance(b, int):
|
||||||
|
return a == b
|
||||||
|
return math.isclose(float(a), float(b), rel_tol=rel, abs_tol=abs_tol)
|
||||||
|
|
||||||
|
|
||||||
|
def source_of(lab_dir: str) -> str:
|
||||||
|
"""Исходный код solution.py — для проверки использованных механизмов."""
|
||||||
|
return (ROOT / lab_dir / "solution.py").read_text(encoding="utf-8")
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 1 (последовательное вычисление)."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from conftest import close, load_solution # noqa: E402
|
||||||
|
from common.tasks import TASKS, run_sequential, smoke_params # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution("lab01_sequential")
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_sequential_correct():
|
||||||
|
mod = load_solution("lab01_sequential")
|
||||||
|
# имя задачи берём из варианта студента
|
||||||
|
from common.tasks import get_variant
|
||||||
|
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
|
||||||
|
sp = smoke_params(name)
|
||||||
|
|
||||||
|
# эталон: последовательный запуск из common
|
||||||
|
expected, _ = run_sequential(name, sp, parts=1)
|
||||||
|
# последовательный запуск из 3 кусков тоже обязан совпасть
|
||||||
|
expected3, _ = run_sequential(name, sp, parts=3)
|
||||||
|
|
||||||
|
got1 = mod.run_cpu_sequential(name, sp, parts=1)
|
||||||
|
got3 = mod.run_cpu_sequential(name, sp, parts=3)
|
||||||
|
|
||||||
|
assert close(got1, expected), f"checksum(parts=1)={got1}, ожидалось {expected}"
|
||||||
|
assert close(got3, expected3), (
|
||||||
|
f"checksum зависит от разбиения: parts=3 -> {got3}, parts=1 -> {expected}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cpu_sequential_returns_time():
|
||||||
|
mod = load_solution("lab01_sequential")
|
||||||
|
from common.tasks import get_variant
|
||||||
|
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
|
||||||
|
res = mod.run_cpu_sequential(name, smoke_params(name), parts=2)
|
||||||
|
assert isinstance(res, tuple) and len(res) == 2, (
|
||||||
|
"run_cpu_sequential должна возвращать (checksum, секунды)")
|
||||||
|
checksum, seconds = res
|
||||||
|
assert isinstance(seconds, (int, float)) and seconds >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_io_sequential_correct():
|
||||||
|
mod = load_solution("lab01_sequential")
|
||||||
|
results, seconds = mod.run_io_sequential(6, 0.0)
|
||||||
|
assert results == [(i, i * i) for i in range(6)]
|
||||||
|
assert seconds >= 0
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 2 (многопоточный код)."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from conftest import load_solution, source_of # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution("lab02_threads")
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_threads_correct():
|
||||||
|
mod = load_solution("lab02_threads")
|
||||||
|
results, seconds = mod.run_io_threads_manual(8, 0.0, p=3)
|
||||||
|
assert [r[0] for r in results] == list(range(8)), (
|
||||||
|
"результаты должны быть упорядочены по item")
|
||||||
|
assert seconds >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_threads_parallel():
|
||||||
|
"""8 запросов по 0.1 c на 4 потоках должны занять ~0.2 c, не ~0.8 c."""
|
||||||
|
mod = load_solution("lab02_threads")
|
||||||
|
_, seconds = mod.run_io_threads_manual(8, 0.1, p=4)
|
||||||
|
assert seconds < 0.6, (
|
||||||
|
f"выполнено за {seconds:.2f} c — похоже, потоки работают по очереди "
|
||||||
|
"(start() до join()!)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_pool_threads_correct():
|
||||||
|
mod = load_solution("lab02_threads")
|
||||||
|
results, seconds = mod.run_io_threads_pool(8, 0.0, p=3)
|
||||||
|
assert [r[0] for r in results] == list(range(8))
|
||||||
|
assert seconds >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_pool_threads_parallel():
|
||||||
|
mod = load_solution("lab02_threads")
|
||||||
|
_, seconds = mod.run_io_threads_pool(8, 0.1, p=4)
|
||||||
|
assert seconds < 0.6, (
|
||||||
|
f"выполнено за {seconds:.2f} c — пул не даёт параллельности")
|
||||||
|
|
||||||
|
|
||||||
|
def test_uses_threading():
|
||||||
|
src = source_of("lab02_threads")
|
||||||
|
assert "threading" in src or "ThreadPoolExecutor" in src, (
|
||||||
|
"в решении должны использоваться потоки")
|
||||||
|
|
||||||
|
|
||||||
|
def test_manual_uses_thread_class():
|
||||||
|
src = source_of("lab02_threads")
|
||||||
|
assert "threading.Thread" in src, (
|
||||||
|
"run_io_threads_manual должна использовать threading.Thread")
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 3 (GIL)."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from conftest import close, load_solution # noqa: E402
|
||||||
|
from common.tasks import run_sequential, smoke_params # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution("lab03_gil")
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_task_on_threads_correct():
|
||||||
|
mod = load_solution("lab03_gil")
|
||||||
|
from common.tasks import get_variant
|
||||||
|
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
|
||||||
|
sp = smoke_params(name)
|
||||||
|
|
||||||
|
expected, _ = run_sequential(name, sp, parts=1)
|
||||||
|
got, seconds = mod.run_task_on_threads(name, sp, p=4)
|
||||||
|
|
||||||
|
assert close(got, expected), f"checksum {got} != {expected}"
|
||||||
|
assert seconds >= 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_task_on_threads_uses_pool():
|
||||||
|
src = (Path(__file__).resolve().parents[1] / "lab03_gil" / "solution.py"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
assert "ThreadPoolExecutor" in src or "threading.Thread" in src, (
|
||||||
|
"нужны именно потоки (ThreadPoolExecutor или Thread)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_measure_cpu_threads_keys():
|
||||||
|
mod = load_solution("lab03_gil")
|
||||||
|
from common.tasks import get_variant
|
||||||
|
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
|
||||||
|
sp = smoke_params(name)
|
||||||
|
res = mod.measure_cpu_threads(name, sp, [1, 2, 4])
|
||||||
|
assert sorted(res.keys()) == [1, 2, 4], f"ключи {sorted(res.keys())}"
|
||||||
|
assert all(t > 0 for t in res.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_measure_gil_release_parallel():
|
||||||
|
"""hashlib на блоках 1 МБ обязан показать S(4) > 1.4."""
|
||||||
|
mod = load_solution("lab03_gil")
|
||||||
|
res = mod.measure_gil_release([1, 4], block_size=1 << 20, blocks_per_part=8)
|
||||||
|
s4 = res[1] / res[4]
|
||||||
|
assert s4 > 1.4, (
|
||||||
|
f"S(4)={s4:.2f} — hashlib не ускорился потоками. Проверьте размер "
|
||||||
|
"блоков (нужны ≥ 256 КБ) и что общий объём делится между потоками")
|
||||||
|
|
||||||
|
|
||||||
|
def test_gil_status():
|
||||||
|
mod = load_solution("lab03_gil")
|
||||||
|
ver, gil = mod.gil_status()
|
||||||
|
assert isinstance(ver, str) and ver
|
||||||
|
assert isinstance(gil, bool)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 4 (multiprocessing).
|
||||||
|
|
||||||
|
Важная особенность: тесты запускают solution.py как ПОДПРОЦЕСС с переменной
|
||||||
|
окружения LAB_SMOKE=1 — это одновременно проверяет и корректность, и
|
||||||
|
Windows-совместимость (наличие if __name__ == "__main__").
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
LAB_DIR = "lab04_multiprocessing"
|
||||||
|
|
||||||
|
|
||||||
|
def run_smoke():
|
||||||
|
"""Запускает solution.py в подпроцессе в smoke-режиме, возвращает JSON."""
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["LAB_SMOKE"] = "1"
|
||||||
|
env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, str(ROOT / LAB_DIR / "solution.py")],
|
||||||
|
capture_output=True, text=True, env=env, timeout=120,
|
||||||
|
cwd=str(ROOT))
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise AssertionError(
|
||||||
|
f"solution.py упал:\n--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}")
|
||||||
|
# smoke-режим печатает одну строку JSON с префиксом LAB_SMOKE_JSON:
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if line.startswith("LAB_SMOKE_JSON:"):
|
||||||
|
return json.loads(line[len("LAB_SMOKE_JSON:"):])
|
||||||
|
raise AssertionError(
|
||||||
|
f"в smoke-режиме не найден LAB_SMOKE_JSON. stdout:\n{proc.stdout}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoke_processes_work():
|
||||||
|
"""Запуск на процессах из подпроцесса: checksum корректен, p>1 отработал."""
|
||||||
|
out = run_smoke()
|
||||||
|
assert out.get("ok") is True, f"smoke-отчёт: {out}"
|
||||||
|
assert out.get("result_ok") is True, (
|
||||||
|
"checksum параллельного запуска не совпал с последовательным")
|
||||||
|
assert out.get("p") and out["p"] >= 2, "smoke должен запускаться при p>=2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoke_overhead_measured():
|
||||||
|
out = run_smoke()
|
||||||
|
ov = out.get("overhead", {})
|
||||||
|
assert len(ov) >= 2, "measure_overhead должна вернуть замеры минимум для 2 размеров"
|
||||||
|
assert all(t > 0 for t in ov.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_uses_dispatch_kernel():
|
||||||
|
src = (ROOT / LAB_DIR / "solution.py").read_text(encoding="utf-8")
|
||||||
|
assert "dispatch_kernel" in src, (
|
||||||
|
"для Pool нужен пиккелируемый dispatch_kernel из common.tasks")
|
||||||
|
assert 'if __name__ == "__main__"' in src, (
|
||||||
|
"обязательна защита if __name__ == '__main__' (Windows/spawn)")
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 5 (синхронизация)."""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from conftest import load_solution # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution("lab05_sync")
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_race_fixed_exact():
|
||||||
|
"""С Lock результат точен при любых запусках."""
|
||||||
|
mod = load_solution("lab05_sync")
|
||||||
|
for _ in range(3):
|
||||||
|
got, expected = mod.race_fixed(6, 10_000)
|
||||||
|
assert got == expected, (
|
||||||
|
f"race_fixed потерял обновления: {got} из {expected} — "
|
||||||
|
"Lock не защищает счётчик")
|
||||||
|
|
||||||
|
|
||||||
|
def test_race_demo_type():
|
||||||
|
mod = load_solution("lab05_sync")
|
||||||
|
got, expected = mod.race_demo(4, 5_000)
|
||||||
|
assert isinstance(got, int) and isinstance(expected, int)
|
||||||
|
assert expected == 20_000
|
||||||
|
assert 0 < got <= expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_semaphore_limits():
|
||||||
|
"""max_concurrent=2 на 10 запросах по 0.1 c должно занять ~0.5 c (не ~0.1)."""
|
||||||
|
mod = load_solution("lab05_sync")
|
||||||
|
res, t = mod.semaphore_limiter(10, 0.1, 2)
|
||||||
|
assert len(res) == 10
|
||||||
|
assert t >= 0.45, (
|
||||||
|
f"выполнено за {t:.2f} c — лимит {2} одновременных не соблюдается")
|
||||||
|
# и параллельность работает: на limiter=10 было бы ~0.1 c
|
||||||
|
res2, t2 = mod.semaphore_limiter(10, 0.1, 10)
|
||||||
|
assert t2 < t * 0.6, "при большем лимите время должно заметно упасть"
|
||||||
|
|
||||||
|
|
||||||
|
def test_producer_consumer_no_deadlock():
|
||||||
|
mod = load_solution("lab05_sync")
|
||||||
|
import signal
|
||||||
|
|
||||||
|
def handler(signum, frame):
|
||||||
|
raise TimeoutError("producer_consumer не завершился — дедлок!")
|
||||||
|
|
||||||
|
try:
|
||||||
|
signal.signal(signal.SIGALRM, handler)
|
||||||
|
signal.alarm(30)
|
||||||
|
processed, total_sum = mod.producer_consumer(2, 3, 30)
|
||||||
|
signal.alarm(0)
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
# Windows: SIGALRM нет — просто вызываем
|
||||||
|
processed, total_sum = mod.producer_consumer(2, 3, 30)
|
||||||
|
|
||||||
|
assert processed == 30, f"обработано {processed} из 30 — задачи потерялись"
|
||||||
|
assert total_sum == sum(range(30)), (
|
||||||
|
f"сумма {total_sum} неверна — данные искажены")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 6 (asyncio)."""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from conftest import load_solution, source_of # noqa: E402
|
||||||
|
|
||||||
|
LAB = "lab06_asyncio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_all_async_correct_and_concurrent():
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
results, seconds = asyncio.run(mod.fetch_all_async(8, 0.1))
|
||||||
|
assert results == [(i, i * i) for i in range(8)], (
|
||||||
|
"результаты должны быть (item, item*item) и упорядочены по item")
|
||||||
|
assert seconds < 0.5, (
|
||||||
|
f"8 запросов по 0.1 c заняли {seconds:.2f} c — конкурентности нет; "
|
||||||
|
"проверьте, что используется asyncio.sleep и await")
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_all_limited_respects_limit():
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
results, seconds = asyncio.run(mod.fetch_all_limited(10, 0.1, 2))
|
||||||
|
assert results == [(i, i * i) for i in range(10)]
|
||||||
|
assert seconds >= 0.45, (
|
||||||
|
f"10 запросов по 0.1 c при лимите 2 заняли {seconds:.2f} c "
|
||||||
|
"— лимит не соблюдается (должно быть ~0.5 c)")
|
||||||
|
# и без лимита было бы ~0.1 c
|
||||||
|
_, t_all = asyncio.run(mod.fetch_all_limited(10, 0.1, 10))
|
||||||
|
assert t_all < seconds * 0.5, "при большем лимите время должно заметно упасть"
|
||||||
|
|
||||||
|
|
||||||
|
def test_blocking_mistake_is_sequential():
|
||||||
|
"""time.sleep в корутине должен дать время ≈ items*delay."""
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
seconds = mod.measure_blocking_mistake(5, 0.1)
|
||||||
|
assert seconds >= 0.45, (
|
||||||
|
f"заняло {seconds:.2f} c вместо ~0.5 c — time.sleep внезапно "
|
||||||
|
"конкурентен? Проверьте, что в _fetch_blocking именно time.sleep")
|
||||||
|
|
||||||
|
|
||||||
|
def test_uses_async_syntax():
|
||||||
|
src = source_of(LAB)
|
||||||
|
assert "async def" in src, "нужны корутины (async def)"
|
||||||
|
assert "asyncio.gather" in src, "нужен asyncio.gather"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 7 (сводная).
|
||||||
|
|
||||||
|
solution.py запускается подпроцессом с LAB_SMOKE=1 (как в лабе 4),
|
||||||
|
т.к. в решении используется multiprocessing.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
LAB_DIR = "lab07_comparison"
|
||||||
|
|
||||||
|
|
||||||
|
def run_smoke():
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["LAB_SMOKE"] = "1"
|
||||||
|
env["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, str(ROOT / LAB_DIR / "solution.py")],
|
||||||
|
capture_output=True, text=True, env=env, timeout=120,
|
||||||
|
cwd=str(ROOT))
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise AssertionError(
|
||||||
|
f"solution.py упал:\n--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}")
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if line.startswith("LAB_SMOKE_JSON:"):
|
||||||
|
return json.loads(line[len("LAB_SMOKE_JSON:"):])
|
||||||
|
raise AssertionError(f"нет LAB_SMOKE_JSON. stdout:\n{proc.stdout}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoke_all_approaches_work():
|
||||||
|
out = run_smoke()
|
||||||
|
assert out.get("ok") is True
|
||||||
|
assert out.get("cpu_ok") is True, (
|
||||||
|
"CPU-подходы дают неверный checksum (потоки и/или процессы)")
|
||||||
|
assert out.get("io_ok") is True, "I/O-подходы вернули неверный объём результатов"
|
||||||
|
|
||||||
|
|
||||||
|
def test_smoke_io_times_present():
|
||||||
|
out = run_smoke()
|
||||||
|
times = out.get("io_times", {})
|
||||||
|
assert all(k in times for k in ("seq", "threads", "async")), times
|
||||||
|
assert all(t >= 0 for t in times.values())
|
||||||
|
|
||||||
|
|
||||||
|
def test_uses_all_four_models():
|
||||||
|
src = (ROOT / LAB_DIR / "solution.py").read_text(encoding="utf-8")
|
||||||
|
assert "ThreadPoolExecutor" in src, "нужны потоки"
|
||||||
|
assert "Pool" in src or "ProcessPoolExecutor" in src, "нужны процессы"
|
||||||
|
assert "asyncio" in src and "gather" in src, "нужен asyncio"
|
||||||
|
assert 'if __name__ == "__main__"' in src, "нужен __main__-guard"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Автотесты лабораторной 8 (MPI).
|
||||||
|
|
||||||
|
Если mpi4py не установлен — тесты пропускаются (SKIPPED), но сама работа
|
||||||
|
сдаётся с запуском через mpiexec -n 2/4 (проверяет преподаватель).
|
||||||
|
|
||||||
|
Тесты выполняются в режиме ОДНОГО ранга (без mpiexec): в этом режиме
|
||||||
|
scatter/gather работают тривиально, но корректность схемы и checksum
|
||||||
|
проверяются. Многоранговую корректность гарантирует эталон и проверка
|
||||||
|
преподавателем через mpiexec.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
mpi4py = pytest.importorskip("mpi4py", reason="mpi4py не установлен")
|
||||||
|
|
||||||
|
from conftest import close, load_solution # noqa: E402
|
||||||
|
from common.tasks import run_sequential, smoke_params # noqa: E402
|
||||||
|
|
||||||
|
LAB = "lab08_mpi"
|
||||||
|
|
||||||
|
|
||||||
|
def test_variant_number_set():
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
|
||||||
|
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpi_run_single_rank():
|
||||||
|
"""Один ранг: checksum должен совпадать с последовательным запуском."""
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
from common.tasks import get_variant
|
||||||
|
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
|
||||||
|
sp = smoke_params(name)
|
||||||
|
|
||||||
|
expected, _ = run_sequential(name, sp, parts=1)
|
||||||
|
got = mod.mpi_run(name, sp)
|
||||||
|
assert got is not None, "mpi_run должна вернуть checksum (на любом ранге)"
|
||||||
|
assert close(got, expected), f"checksum {got} != {expected}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpi_pi():
|
||||||
|
mod = load_solution(LAB)
|
||||||
|
pi = mod.mpi_pi(100_000)
|
||||||
|
assert 3.0 < pi < 3.3, f"pi = {pi} — вне допустимого диапазона для 100k бросков"
|
||||||
Reference in new issue
Block a user