← Назад к вопросам

Как переключается контекст в 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
Как переключается контекст в multithreading (многопоточности)? | PrepBro