Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -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` — функция вызвалась в главном потоке.
|
||||
Reference in new issue
Block a user