Files
parallel-programming/lab03_gil/solution.py
T
2026-10-07 13:06:47 +05:00

116 lines
5.1 KiB
Python

# -*- 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()