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

В какой момент происходит переключение контекста

3.0 Senior🔥 131 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Переключение контекста в ОС и Python

Краткий ответ

Переключение контекста — это момент, когда операционная система прерывает выполнение одного потока/процесса и начинает выполнять другой. Это ключевой концепт для понимания многопроцессности.

На уровне ОС: когда происходит переключение контекста

1. Time slice (квант времени) истёк

Это самый частый случай. ОС дала потоку N миллисекунд процессорного времени, и когда это время истекло — переключилась на следующий поток.

Временная шкала (на одном ядре CPU):
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ Thread 1    │ Thread 2    │ Thread 1    │ Thread 3    │
│ (10ms)      │ (10ms)      │ (10ms)      │ (10ms)      │
└─────────────┴─────────────┴─────────────┴─────────────┘
 ↑             ↑             ↑             ↑
 переключение  переключение  переключение  переключение
 контекста     контекста     контекста     контекста

2. Блокирующая операция I/O

Поток ждёт данные из:

  • Диска (чтение файла)
  • Сети (HTTP запрос)
  • Стандартного ввода (input())

В этот момент поток блокируется, и ОС немедленно переключается на другой готовый поток.

# Пример 1: Блокирующее I/O
import time

def read_file():
    with open("large_file.txt", "r") as f:
        data = f.read()  # БЛОКИРУЕТ! (может быть 100ms)
    return len(data)

def cpu_work():
    return sum(range(1000000))

# На одном ядре:
start = time.time()
result1 = read_file()      # Ждёт 100ms, поток блокирован
result2 = cpu_work()       # Пока 1-й ждёт, выполняется этот
elapsed = time.time() - start
# Примерно 100ms + время CPU работы

3. Состояние готовности (ready state)

Поток готов к выполнению (не блокирован), но ОС решила дать процессорное время другому потоку.

Состояния потока:

Running → (time slice истёк) → Ready → (планировщик выбрал) → Running
                            ↑
                     Могут быть N потоков
                     в очереди Ready

Running → (вызвал I/O) → Waiting → (I/O завершилось) → Ready → Running

4. Синхронизация (Lock, Semaphore)

Поток пытается захватить блокировку, которая занята другим потоком.

import threading

lock = threading.Lock()

def task(thread_id):
    print(f"Поток {thread_id}: пытаюсь захватить lock")
    with lock:  # Если lock занят — переключение контекста!
        print(f"Поток {thread_id}: захватил lock")
        time.sleep(1)
    print(f"Поток {thread_id}: отпустил lock")

threads = [threading.Thread(target=task, args=(i,)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# Вывод:
# Поток 0: пытаюсь захватить lock
# Поток 0: захватил lock
# Поток 1: пытаюсь захватить lock
# Поток 2: пытаюсь захватить lock
# (ждут 1 секунду)
# Поток 0: отпустил lock
# Поток 1: захватил lock
# ...

На уровне Python: особенности GIL

Global Interpreter Lock (GIL)

В CPython есть Global Interpreter Lock — мьютекс, который гарантирует, что одновременно выполняется только один поток.

import threading
import time

def cpu_work(thread_id):
    """CPU-bound задача"""
    start = time.time()
    result = 0
    for i in range(100_000_000):
        result += i
    elapsed = time.time() - start
    print(f"Поток {thread_id}: {elapsed:.2f}s")

# Один поток
start = time.time()
cpu_work(1)
print(f"Один поток: {time.time() - start:.2f}s")

# Два потока (но GIL замедлит!)
start = time.time()
t1 = threading.Thread(target=cpu_work, args=(1,))
t2 = threading.Thread(target=cpu_work, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Два потока: {time.time() - start:.2f}s")

# На четырехядерной машине:
# Один поток: 5.30s
# Два потока: 10.60s (медленнее! из-за GIL)

Когда GIL освобождается?

GIL освобождается в точках:

  1. После bytecode инструкции (примерно каждые 15 инструкций в Python 3.2+)
def simple_func():
    x = 1  # GIL может быть освобождён
    y = 2  # GIL может быть освобождён
    z = x + y  # GIL может быть освобождён
  1. Во время I/O операций (это позволяет потокам параллелиться)
import urllib.request

def fetch_url(url):
    # Здесь GIL освобождается!
    response = urllib.request.urlopen(url)
    return len(response.read())

# Два потока действительно будут работать параллельно (I/O bound)
  1. В C-коде расширений
import numpy as np

def numpy_work():
    # NumPy освобождает GIL для параллельной работы
    arr = np.zeros((10000, 10000))
    result = np.sum(arr)

Практические примеры переключения контекста

Пример 1: Time slice переключение

import threading
import time

def count_to(n, name):
    for i in range(n):
        if i % 3 == 0:
            time.sleep(0)  # Явно даём другим потокам шанс
        print(f"{name}: {i}")

t1 = threading.Thread(target=count_to, args=(5, "A"))
t2 = threading.Thread(target=count_to, args=(5, "B"))
t1.start()
t2.start()
t1.join()
t2.join()

# Вывод (может быть разным каждый раз):
# A: 0
# B: 0
# A: 1
# B: 1
# ...
# (порядок чередуется из-за переключения контекста)

Пример 2: I/O блокирует, другие работают

import threading
import time

def read_slow_resource(name):
    print(f"{name}: начинаю читать")
    time.sleep(2)  # Имитируем I/O (диск, сеть)
    print(f"{name}: закончил читать")

def cpu_work(name):
    print(f"{name}: начинаю CPU работу")
    for i in range(3):
        time.sleep(0.5)
        print(f"{name}: CPU шаг {i}")
    print(f"{name}: закончил CPU работу")

t1 = threading.Thread(target=read_slow_resource, args=("Reader",))
t2 = threading.Thread(target=cpu_work, args=("CPU",))
t1.start()
t2.start()
t1.join()
t2.join()

# Вывод:
# Reader: начинаю читать
# CPU: начинаю CPU работу
# CPU: CPU шаг 0
# CPU: CPU шаг 1
# CPU: CPU шаг 2
# Reader: закончил читать
# CPU: закончил CPU работу
# (ура! CPU работал параллельно с I/O, благодаря переключению контекста)

Пример 3: Multiprocessing (обходим GIL)

import multiprocessing
import time

def cpu_work(name):
    start = time.time()
    result = sum(range(100_000_000))
    elapsed = time.time() - start
    print(f"{name}: {elapsed:.2f}s")

# Multiprocessing (разные процессы, нет GIL!)
start = time.time()
p1 = multiprocessing.Process(target=cpu_work, args=("Process 1",))
p2 = multiprocessing.Process(target=cpu_work, args=("Process 2",))
p1.start()
p2.start()
p1.join()
p2.join()
print(f"Multiprocessing: {time.time() - start:.2f}s")

# На четырёхядерной машине:
# Threads: 10.60s (GIL замедляет)
# Multiprocessing: 5.30s (параллельная работа на разных ядрах)

Когда переключение контекста дорого обходится?

import time

# Пример: очень частое переключение контекста

def many_thread_switches():
    """N потоков вызывают lock миллион раз"""
    lock = threading.Lock()
    
    def increment():
        for _ in range(1_000_000):
            with lock:  # Переключение контекста каждый раз!
                pass
    
    threads = [threading.Thread(target=increment) for _ in range(4)]
    start = time.time()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    return time.time() - start

elapsed = many_thread_switches()
print(f"4 потока * 1M lock операций = {elapsed:.2f}s")
# Результат: очень медленно! (может быть 10+ секунд)

Оптимизация: уменьшение переключений контекста

# Плохо: много переключаний
results = []
lock = threading.Lock()
for i in range(1_000_000):
    with lock:
        results.append(i)

# Хорошо: батч операции
results = []
lock = threading.Lock()
batch = []
for i in range(1_000_000):
    batch.append(i)
    if len(batch) == 1000:
        with lock:
            results.extend(batch)
        batch = []

Вывод

Переключение контекста происходит:

  1. По времени — time slice истёк
  2. По I/O — поток ждёт диск/сеть
  3. По синхронизации — lock занят
  4. По yield — явное освобождение

Важно помнить:

  • Много переключений = overhead
  • I/O bound операции выигрывают от многопоточности
  • CPU bound операции нуждаются в multiprocessing (из-за GIL)
  • Переключение контекста — это нормальное явление, его можно оптимизировать, но полностью избежать нельзя
В какой момент происходит переключение контекста | PrepBro