← Назад к вопросам
Как переключается контекст в multithreading (многопоточности)?
2.0 Middle🔥 151 комментариев
#Python Core#Soft Skills
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Context Switching в Multithreading
Context Switching (переключение контекста) — это механизм, который позволяет процессору переходить от одного потока к другому. Это фундаментальная концепция для понимания многопоточности.
1. Что такое контекст
Контекст потока (Thread Context) — это текущее состояние потока:
# Когда поток выполняется, его контекст включает:
# 1. Значения регистров процессора
# 2. Программный счётчик (PC) — какую строку кода выполняем
# 3. Стек вызовов функций
# 4. Локальные переменные
# 5. Состояние памяти (в какой момент выполнения находимся)
def thread_work():
x = 10 # ← Контекст содержит x
y = 20 # ← После выполнения этой строки, контекст обновляется
z = x + y # ← Контекст содержит значения x, y
2. Как переключение контекста происходит
Процесс:
1. Операционная система решает переключить контекст (по таймеру)
2. Текущий контекст Потока A сохраняется в памяти
3. Контекст Потока B загружается из памяти
4. Процессор продолжает выполнение с того места, где прервался Поток B
5. Когда приходит время, снова переключаемся на Поток A
Визуализация:
Время →
Поток A: выполнение | сохранение контекста | ждёт
Поток B: ждёт | загрузка контекста | выполнение
Поток C: ждёт | ждёт | загрузка контекста
Процессор работает только с одним потоком в конкретный момент,
но очень быстро переключается между ними.
3. Пример: видимость переключения контекста
import threading
import time
def thread_function(name: str):
for i in range(3):
print(f"[{name}] Итерация {i}, tid={threading.current_thread().ident}")
time.sleep(0.001) # Даём другим потокам шанс выполняться
thread1 = threading.Thread(target=thread_function, args=("Thread-1",))
thread2 = threading.Thread(target=thread_function, args=("Thread-2",))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# Вывод (порядок может быть разным из-за переключений):
# [Thread-1] Итерация 0, tid=4444
# [Thread-2] Итерация 0, tid=4445
# [Thread-1] Итерация 1, tid=4444
# [Thread-2] Итерация 1, tid=4445
# ...
Каждый раз вывод может быть разным! Это результат переключения контекста.
4. Добровольное и принудительное переключение
Добровольное (Cooperative)
import threading
def cooperative_task():
for i in range(5):
print(f"Работаю... {i}")
threading.Event().wait(0.1) # Добровольно уступаем время
thread = threading.Thread(target=cooperative_task)
thread.start()
thread.join()
Принудительное (Preemptive)
def non_cooperative_task():
for i in range(5):
# Работаем без сна — ОС всё равно переключит нас
x = 0
for _ in range(1000000):
x += 1
print(f"Итерация {i}")
thread = threading.Thread(target=non_cooperative_task)
thread.start()
thread.join()
# ОС автоматически переключит контекст, даже если мы не просим
5. GIL (Global Interpreter Lock) в Python
Важно: В Python происходит что-то особенное:
import threading
import time
def cpu_bound_task(name: str):
"""Задача, требующая процессора (CPU-bound)."""
total = 0
for i in range(100_000_000):
total += i
print(f"{name} завершён, результат: {total}")
start = time.time()
# Один поток
print("=== Один поток ===")
cpu_bound_task("Single")
print(f"Время: {time.time() - start:.2f}s")
# Два потока
print("\n=== Два потока ===")
start = time.time()
thread1 = threading.Thread(target=cpu_bound_task, args=("Thread-1",))
thread2 = threading.Thread(target=cpu_bound_task, args=("Thread-2",))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Время: {time.time() - start:.2f}s")
# Результат:
# Один поток: ~3s
# Два потока: ~6s (не ~1.5s, как ожидали бы!)
# Причина: GIL — только один поток может выполнять Python код одновременно
Что такое GIL:
- Global Interpreter Lock — глобальная блокировка интерпретатора
- Позволяет только одному потоку выполнять Python код одновременно
- Переключение всё равно происходит, но многопоточность не даёт выигрыша для CPU-bound задач
6. Когда многопоточность полезна в Python
I/O-bound задачи (I/O-bound) — с GIL помощь есть:
import threading
import time
import requests
def fetch_url(url: str):
"""I/O-bound: ждём сети, GIL отпускается."""
print(f"Начало загрузки {url}")
response = requests.get(url) # Ждём ответа сервера
print(f"Загружен {url}: {len(response.content)} байт")
return response.text
start = time.time()
# С многопоточностью
threads = []
for url in ["http://example.com", "http://example.org", "http://example.net"]:
thread = threading.Thread(target=fetch_url, args=(url,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f"Время с потоками: {time.time() - start:.2f}s")
# Время ~ 2s (все загружаются параллельно)
Без многопоточности было бы:
# Последовательно
for url in urls:
fetch_url(url)
# Время ~ 6s (3 × 2s)
7. Context Switching в низком уровне
Что ОС сохраняет при переключении:
// На уровне C/ОС
struct ThreadContext {
uint64_t registers[16]; // Значения всех регистров
uint64_t program_counter; // Куда вернуться
uint64_t stack_pointer; // Указатель стека
uint64_t instruction_pointer;// Текущая инструкция
// ... и многое другое
};
// При переключении контекста ОС делает:
// 1. Сохраняет current_context
// 2. Загружает next_context
// 3. Возвращает управление next_thread с его контекстом
8. Проблемы, вызванные Context Switching
Race Condition
import threading
counter = 0
def increment():
global counter
for _ in range(1000000):
counter += 1 # Это не атомарно!
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter) # Ожидаем 2000000, но получим меньше!
# Причина: между проверкой и изменением контекст может переключиться
Решение: Lock
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(1000000):
with lock: # Блокируем доступ
counter += 1
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(counter) # 2000000 ✓
9. Стоимость Context Switching
import time
import threading
# Частое переключение дорогое
def expensive_switching():
for i in range(1000):
time.sleep(0.0001) # Даём другим потокам шанс
start = time.time()
for _ in range(100):
expensive_switching()
print(f"Время с частыми переключениями: {time.time() - start:.2f}s")
# Редкое переключение дешёво
def cheap_switching():
for i in range(1000):
pass # Пусть ОС сама решает, когда переключаться
start = time.time()
for _ in range(100):
cheap_switching()
print(f"Время с редкими переключениями: {time.time() - start:.2f}s")
# Вывод: частые переключения замедляют работу!
10. Альтернатива многопоточности
asyncio (async/await) — лучше для I/O:
import asyncio
import aiohttp
async def fetch_url(url: str):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
# Все запросы выполняются параллельно в одном потоке!
tasks = [
fetch_url("http://example.com"),
fetch_url("http://example.org"),
fetch_url("http://example.net")
]
results = await asyncio.gather(*tasks)
print(f"Загружено {len(results)} страниц")
asyncio.run(main())
# Нет Context Switching между потоками — всё в одном потоке!
# Переключение только когда встречаем await (сотни тысяч раз в секунду, не проблема)
Резюме
Context Switching:
- ОС сохраняет состояние потока (регистры, стек, счётчик)
- Загружает состояние другого потока
- Процессор продолжает работу с новым потоком
В Python:
- GIL позволяет только одному потоку выполнять Python код
- Многопоточность полезна для I/O-bound задач
- Для CPU-bound используй multiprocessing или asyncio
- Context Switching имеет накладные расходы
Правило:
- I/O-bound задачи → Threading или asyncio
- CPU-bound задачи → multiprocessing или asyncio
- Избегай Race Conditions через Locks и Semaphores