Init
This commit is contained in:
commit
c394677d07
59 files changed
+5149
No files matched your search
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: GIL изнутри — интервал переключения и stat.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab03_gil/examples/01_gil_basics_demo.py
|
||||
|
||||
Показывает:
|
||||
- sys.getswitchinterval() — как часто потоки уступают GIL;
|
||||
- sys._is_gil_enabled() (Python 3.13+) — статус GIL;
|
||||
- поток-«наблюдатель» работает, пока главный поток занят счётом
|
||||
(переключения есть!), но суммарная производительность не растёт.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
|
||||
def burn(n):
|
||||
s = 0.0
|
||||
for i in range(n):
|
||||
s += i ** 0.5
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
if hasattr(sys, "_is_gil_enabled"):
|
||||
print(f"GIL включён: {sys._is_gil_enabled()}")
|
||||
print(f"интервал переключения GIL: {sys.getswitchinterval() * 1000:.0f} мс\n")
|
||||
|
||||
N = 4_000_000
|
||||
|
||||
# 1. Один поток
|
||||
t0 = time.perf_counter()
|
||||
burn(N)
|
||||
t1 = time.perf_counter() - t0
|
||||
print(f"1 поток: {t1:.3f} c")
|
||||
|
||||
# 2. Поток-наблюдатель: он живёт (переключения идут), но работе это не помогает
|
||||
ticks = [0]
|
||||
|
||||
def observer(stop_flag):
|
||||
while not stop_flag[0]:
|
||||
ticks[0] += 1
|
||||
time.sleep(0.005)
|
||||
|
||||
stop = [False]
|
||||
obs = threading.Thread(target=observer, args=(stop,), daemon=True)
|
||||
obs.start()
|
||||
|
||||
t0 = time.perf_counter()
|
||||
burn(N) # главный поток считает
|
||||
t2 = time.perf_counter() - t0
|
||||
stop[0] = True
|
||||
obs.join()
|
||||
print(f"1 поток + наблюдатель: {t2:.3f} c (наблюдатель проснулся "
|
||||
f"{ticks[0]} раз) — работа не замедлилась: переключения дешёвые")
|
||||
|
||||
# 3. Два считающих потока: время то же, работы вдвое больше — но не быстрее
|
||||
t0 = time.perf_counter()
|
||||
threads = [threading.Thread(target=burn, args=(N // 2,)) for _ in range(2)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
t4 = time.perf_counter() - t0
|
||||
print(f"2 потока по {N // 2}: {t4:.3f} c => S={t1 / t4:.2f} (GIL!)")
|
||||
print("\nВывод: переключения GIL происходят, но байт-код всё равно")
|
||||
print("исполняется по очереди — суммарная скорость не растёт.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: hashlib освобождает GIL — потоки дают настоящее ускорение.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab03_gil/examples/02_hashlib_gil_demo.py
|
||||
|
||||
hashlib.sha256 отпускает GIL на время хеширования ОДНОГО блока. На коротких
|
||||
блоках (4 КБ) вызовы слишком быстрые — GIL забирается обратно, ускорения нет.
|
||||
На больших блоках (1 МБ) вызовы длинные — потоки работают параллельно.
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
# Три набора данных (для hashlib — одинаковый суммарный объём, разные блоки)
|
||||
PY_DATA = [os.urandom(256 * 1024) for _ in range(16)] # 4 МБ, для чистого Python
|
||||
HASH_BIG = [os.urandom(1 << 20) for _ in range(48)] # 48 МБ блоками по 1 МБ
|
||||
HASH_SMALL = [os.urandom(4096) for _ in range(48 * 256)] # те же 48 МБ блоками по 4 КБ
|
||||
|
||||
MASK = 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def py_checksum(chunk):
|
||||
"""Чистый Python: GIL не отпускает."""
|
||||
acc = 0
|
||||
for b in chunk:
|
||||
for x in b: # питоновский цикл по байтам
|
||||
acc = (acc + x * 31) & MASK
|
||||
return acc
|
||||
|
||||
|
||||
def hash_checksum(chunk):
|
||||
"""C-функция hashlib: GIL отпускается на время хеширования блока."""
|
||||
acc = 0
|
||||
h = hashlib.sha256
|
||||
for b in chunk:
|
||||
acc = (acc + int.from_bytes(h(b).digest()[:8], "big")) & MASK
|
||||
return acc
|
||||
|
||||
|
||||
def run_threads(fn, data, parts):
|
||||
bounds = [i * len(data) // parts for i in range(parts + 1)]
|
||||
chunks = [data[bounds[i]:bounds[i + 1]] for i in range(parts)]
|
||||
with ThreadPoolExecutor(max_workers=parts) as pool:
|
||||
return list(pool.map(fn, chunks))
|
||||
|
||||
|
||||
def min_time(fn, data, parts, repeats=3):
|
||||
best = float("inf")
|
||||
for _ in range(repeats):
|
||||
t0 = time.perf_counter()
|
||||
run_threads(fn, data, parts)
|
||||
best = min(best, time.perf_counter() - t0)
|
||||
return best
|
||||
|
||||
|
||||
def bench(label, fn, data):
|
||||
acc1 = sum(run_threads(fn, data, 1)) & MASK
|
||||
acc4 = sum(run_threads(fn, data, 4)) & MASK
|
||||
assert acc1 == acc4, "результат зависит от разбиения — ошибка!"
|
||||
t1 = min_time(fn, data, 1)
|
||||
t4 = min_time(fn, data, 4)
|
||||
print(f"{label:<26} T(1)={t1:.3f} c T(4)={t4:.3f} c S(4)={t1 / t4:.2f}")
|
||||
|
||||
|
||||
def main():
|
||||
print("hashlib: одинаковый суммарный объём 48 МБ, разные размеры блоков\n")
|
||||
bench("чистый Python", py_checksum, PY_DATA)
|
||||
bench("hashlib, блоки 4 КБ", hash_checksum, HASH_SMALL)
|
||||
bench("hashlib, блоки 1 МБ", hash_checksum, HASH_BIG)
|
||||
print("\nS(4)≈1 у чистого Python и мелких блоков против S(4)≈ядрам у")
|
||||
print("больших блоков. GIL отпускается на время ОДНОГО вызова C-функции —")
|
||||
print("вызовы должны быть длинными.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Демо: sys.setswitchinterval — интервал переключения GIL.
|
||||
|
||||
Запуск из корня курса:
|
||||
python lab03_gil/examples/03_switch_interval_demo.py
|
||||
|
||||
Уменьшение интервала переключения не добавляет параллельности (GIL всё равно
|
||||
один), но увеличивает накладные расходы: слишком частые переключения
|
||||
замедляют работу.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
|
||||
def burn(n):
|
||||
s = 0.0
|
||||
for i in range(n):
|
||||
s += i ** 0.5
|
||||
return s
|
||||
|
||||
|
||||
def bench(interval, n_threads, n_total):
|
||||
sys.setswitchinterval(interval)
|
||||
bounds = [i * n_total // n_threads for i in range(n_threads + 1)]
|
||||
chunks = [bounds[i + 1] - bounds[i] for i in range(n_threads)]
|
||||
t0 = time.perf_counter()
|
||||
with ThreadPoolExecutor(max_workers=n_threads) as pool:
|
||||
pool.map(burn, chunks)
|
||||
return time.perf_counter() - t0
|
||||
|
||||
|
||||
def main():
|
||||
N = 4_000_000
|
||||
print(f"{'интервал':>10} {'T(1 поток)':>12} {'T(4 потока)':>12}")
|
||||
for interval in (0.005, 0.0005, 0.00005):
|
||||
t1 = bench(interval, 1, N)
|
||||
t4 = bench(interval, 4, N)
|
||||
print(f"{interval * 1000:>9.1f}мс {t1:>11.3f}c {t4:>11.3f}c"
|
||||
f" S(4)={t1 / t4:.2f}")
|
||||
sys.setswitchinterval(0.005) # вернуть значение по умолчанию
|
||||
print("\nВывод: интервал переключения влияет на накладные расходы,")
|
||||
print("но параллельности не добавляет — держатель GIL всё равно один.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user