Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -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. Ответы на контрольные вопросы
|
||||
|
||||
(Из конца методички. Полными предложениями, своими словами.)
|
||||
Reference in new issue
Block a user