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

No files matched your search

+41
View File
@@ -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.
+166
View File
@@ -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
View File
@@ -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),
}
+66
View File
@@ -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 и отразите это в отчёте.
+109
View File
@@ -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`) — чужой вариант виден сразу.
+36
View File
@@ -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-заглушек
+70
View File
@@ -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`;
- при необходимости — столбчатая диаграмма времени.
Вставьте сюда картинки:
![ускорение](speedup.png)
## 6. Анализ
Ответьте письменно (по 2��5 предложений на пункт):
1. Получено ли ускорение? На сколько? Совпадает ли с ожиданиями?
2. Почему именно такая модель (потоки/процессы/async) подходит или не
подходит вашей задаче?
3. Наблюдались ли накладные расходы (запуск процессов, передача данных,
синхронизация)? Где они видны в замерах?
4. Что ограничивает дальнейший рост ускорения (GIL, память, число ядер,
доля последовательного кода)?
## 7. Выводы
(3–6 предложений: что изучено, какие практические выводы сделаны.)
## 8. Ответы на контрольные вопросы
(Из конца методички. Полными предложениями, своими словами.)