Открыть сервис

Кооперативная многозадачность

Кооперативная многозадачность (англ. cooperative multitasking) — это метод организации параллельного выполнения нескольких задач (процессов или потоков) в вычислительной системе, при котором операционная система или среда выполнения не прерывает принудительно выполняющуюся задачу для переключения на другую, а передаёт управление на добровольной основе. В отличие от вытесняющей многозадачности, где планировщик использует таймеры и прерывания для принудительного переключения контекста, в кооперативной модели каждая задача сама решает, когда уступить процессорное время другим задачам, обычно в точках, где она не может продолжать работу (например, при ожидании ввода-вывода) или после завершения определённого логического блока.

История

Ранние вычислительные системы

Кооперативная многозадачность возникла на заре развития операционных систем, когда ресурсы процессора были крайне ограничены, а сложность планирования — минимальной. Первые многозадачные системы, такие как операционная система THE (Technische Hogeschool Eindhoven), разработанная Эдсгером Дейкстрой в 1968 году, использовали именно кооперативный подход. В этих системах задачи выполнялись последовательно, а переключение происходило только при явном вызове специальных функций (например, yield или wait).

Эпоха однозадачных и ранних многозадачных ОС

В 1970–1980-х годах кооперативная многозадачность стала стандартом для многих операционных систем, работающих на персональных компьютерах. Классическим примером является Mac OS (до версии 9 включительно), где приложения должны были регулярно вызывать системные функции для обработки событий и переключения контекста, иначе вся система «зависала» до завершения задачи. Аналогично работала Windows 3.x (Windows 3.0, 3.1, 3.11), где все 16-битные приложения выполнялись в одном адресном пространстве, а переключение между ними происходило только при вызове функций API (например, GetMessage или PeekMessage). Если приложение не возвращало управление (например, входило в бесконечный цикл), вся система становилась неотзывчивой.

Переход к вытесняющей многозадачности

С ростом требований к надёжности и интерактивности, а также с развитием аппаратных средств (появление таймеров и контроллеров прерываний), кооперативная многозадачность начала уступать место вытесняющей. В Windows NT (1993) и Windows 95 (1995) для 32-битных приложений была реализована вытесняющая многозадачность, хотя 16-битные приложения по-прежнему работали в кооперативном режиме внутри виртуальной машины. В macOS X (2001) на основе микроядра XNU, унаследованного от Mach и FreeBSD, была внедрена вытесняющая многозадачность. В Linux изначально использовалась кооперативная многозадачность (в версиях до 2.6), но с 2003 года (в версии 2.6) был внедрён полностью вытесняющий планировщик O(1), а затем CFS (Completely Fair Scheduler).

Современное применение

Несмотря на уход из мейнстримных операционных систем, кооперативная многозадачность остаётся востребованной в специализированных областях:

  • Встраиваемые системы (real-time OS, например, FreeRTOS, uC/OS) — где задачи могут быть жёстко синхронизированы и не требуют принудительного прерывания.
  • Языки программирования (Python, Ruby, JavaScript) — где кооперативная многозадачность реализована через корутины (coroutines) и асинхронное программирование (например, async/await в Python 3.5+).
  • Веб-серверы (Nginx, Node.js) — где один процесс (или поток) обрабатывает множество запросов, переключаясь между ними при блокировках ввода-вывода.

Принцип работы

Основные понятия

  • Задача (task) — минимальная единица выполнения, которая может быть процессом, потоком или корутиной.
  • Планировщик (scheduler) — компонент системы, управляющий очередью задач и переключением контекста. В кооперативной модели планировщик обычно пассивен: он не прерывает задачу, а ждёт, пока та сама вызовет функцию переключения.
  • Точка переключения (yield point) — место в коде задачи, где она добровольно передаёт управление планировщику. Обычно это вызовы yield(), sleep(), wait(), read(), write() или возврат из обработчика событий.

Механизм переключения

  1. Задача A выполняется, пока не достигнет точки переключения (например, вызов yield()).
  2. Планировщик сохраняет контекст задачи A (регистры, стек, счётчик команд) в специальную структуру.
  3. Планировщик выбирает следующую задачу B из очереди готовых к выполнению.
  4. Восстанавливается контекст задачи B, и выполнение продолжается с места, где она была прервана.
  5. Если задача B не вызывает yield() и не завершается, она монопольно занимает процессор, блокируя все остальные задачи.

Особенности реализации

  • Однопоточность — кооперативная многозадачность часто реализуется в одном потоке операционной системы, что упрощает синхронизацию (нет гонок данных), но не позволяет использовать многоядерные процессоры для параллельного выполнения задач.
  • Неблокирующий ввод-вывод — чтобы избежать зависания всей системы, задачи должны использовать неблокирующие операции ввода-вывода (например, select(), epoll(), kqueue()) или асинхронные вызовы.
  • Стек задач — каждая задача может иметь собственный стек (в случае корутин — обычно небольшой, до нескольких килобайт), что позволяет эффективно управлять памятью.

Сравнение с вытесняющей многозадачностью

ХарактеристикаКооперативная многозадачностьВытесняющая многозадачность
Управление переключениемДобровольное (задача сама решает, когда уступить)Принудительное (планировщик использует таймеры и прерывания)
Сложность реализацииНизкая (не требуется аппаратная поддержка прерываний)Высокая (требуется управление прерываниями, защита контекста)
НадёжностьНизкая (одна зависшая задача блокирует всю систему)Высокая (зависшая задача может быть принудительно завершена)
ПроизводительностьВысокая (нет накладных расходов на частые прерывания)Средняя (накладные расходы на переключение контекста по таймеру)
СинхронизацияПростая (нет гонок данных в одном потоке)Сложная (требуются мьютексы, семафоры, критические секции)
Использование многоядерностиОграниченное (обычно один поток ОС)Полное (возможно параллельное выполнение на разных ядрах)
ПримерыMac OS 9, Windows 3.x, Python asyncio, Node.jsWindows NT, Linux, macOS X, Java Threads

Применение

В операционных системах

  • Windows 3.x — классический пример кооперативной многозадачности для 16-битных приложений. Все приложения выполнялись в одном адресном пространстве, и переключение происходило только при вызове функций API (например, GetMessage). Это приводило к тому, что одно приложение, не обрабатывающее сообщения, блокировало всю систему.
  • Mac OS 9 — использовала кооперативную многозадачность для приложений, хотя сама операционная система могла использовать вытесняющую многозадачность для системных процессов (например, для управления памятью).
  • FreeRTOS — популярная операционная система реального времени для встраиваемых систем, где задачи могут быть как кооперативными, так и вытесняющими, в зависимости от конфигурации.

В языках программирования

  • Python — модуль asyncio (Python 3.4+) реализует кооперативную многозадачность с помощью корутин. Корутина передаёт управление планировщику при вызове await на асинхронной операции (например, await asyncio.sleep(1)). Это позволяет обрабатывать тысячи одновременных соединений в одном потоке.
  • JavaScript — среда выполнения Node.js использует цикл событий (event loop), который является кооперативным планировщиком. Каждая функция обратного вызова (callback) выполняется до завершения, а затем управление возвращается циклу. Если callback выполняет длительную синхронную операцию (например, while(true){}), весь процесс зависает.
  • Ruby — библиотека Fiber (Ruby 1.9+) предоставляет легковесные корутины, которые могут добровольно передавать управление друг другу. Однако в стандартной реализации Ruby (MRI) глобальная блокировка интерпретатора (GIL) не позволяет использовать параллелизм на уровне ядер.
  • Go — горутины (goroutines) в Go формально являются кооперативными, но среда выполнения Go может принудительно прерывать их в точках, где они не могут продолжить работу (например, при вызове runtime.Gosched() или при блокировке ввода-вывода). Однако в Go также есть вытесняющие механизмы (начиная с версии 1.14), которые позволяют прерывать горутины по таймеру.

В веб-серверах

  • Nginx — использует асинхронную, событийно-ориентированную архитектуру, где один рабочий процесс (worker process) обрабатывает множество соединений с помощью кооперативной многозадачности. Каждое соединение обрабатывается в рамках одного цикла событий, и переключение между соединениями происходит при ожидании ввода-вывода.
  • Node.js — аналогично Nginx, использует однопоточный цикл событий, где все операции ввода-вывода выполняются асинхронно, а колбэки вызываются по завершении. Это позволяет обрабатывать десятки тысяч одновременных запросов без создания потоков.

В играх и графических приложениях

  • Unity — в игровом движке Unity корутины (coroutines) реализуют кооперативную многозадачность. Корутина может приостанавливать своё выполнение на определённое количество кадров или секунд с помощью yield return new WaitForSeconds(1.0f). Это удобно для создания временных задержек, анимаций и последовательностей действий.
  • Adobe Flash — среда выполнения Flash Player использовала кооперативную многозадачность для выполнения ActionScript. Каждый фрейм (кадр) обрабатывался последовательно, и если один фрейм выполнялся слишком долго, анимация «заикалась».

Преимущества и недостатки

Преимущества

  • Простота реализации — не требуется сложная аппаратная поддержка прерываний и управления контекстом.
  • Низкие накладные расходыпереключение контекста происходит только по необходимости, без периодических прерываний таймера.
  • Отсутствие гонок данных — в однопоточном исполнении нет необходимости в синхронизации доступа к общим данным.
  • Детерминированность — порядок выполнения задач предсказуем, что важно для систем реального времени.

Недостатки

  • Ненадёжность — одна задача, не возвращающая управление, блокирует всю систему.
  • Сложность отладки — зависание системы может быть вызвано ошибкой в любой задаче.
  • Ограниченная масштабируемость — неэффективно на многоядерных процессорах без дополнительных механизмов (например, нескольких потоков ОС).
  • Требования к дисциплине программиста — разработчик должен явно указывать точки переключения, что увеличивает риск ошибок.

Интересные факты

  • Термин «кооперативная многозадачность» иногда называют «невытесняющей многозадачностью» (non-preemptive multitasking), хотя в русскоязычной литературе чаще используется первый вариант.
  • В операционной системе OS/2 (IBM, 1987) была реализована гибридная модель: для 16-битных приложений — кооперативная, для 32-битных — вытесняющая.
  • В Windows 9x (95, 98, Me) 16-битные приложения работали в кооперативном режиме внутри виртуальной машины, а 32-битные — в вытесняющем. Это приводило к тому, что одно «зависшее» 16-битное приложение могло заблокировать все 16-битные приложения, но не затрагивало 32-битные.
  • В языке C++ библиотека Boost.Coroutine (и её наследник C++20 Coroutines) предоставляет средства для кооперативной многозадачности на уровне языка, позволяя писать асинхронный код в синхронном стиле.

Источники

  • Таненбаум Э., Бос Х. «Современные операционные системы» (4-е издание). — СПб.: Питер, 2015.
  • Стивенс У. Р. «UNIX. Разработка сетевых приложений». — М.: Вильямс, 2003.
  • Официальная документация Python: модуль asyncio.
  • Официальная документация Node.js: цикл событий (Event Loop).
  • Документация FreeRTOS: «Cooperative Multitasking».
  • «Operating Systems: Design and Implementation» by Andrew S. Tanenbaum, Albert S. Woodhull. — Prentice Hall, 2006.

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →