Кооперативная многозадачность
Кооперативная многозадачность (англ. 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()или возврат из обработчика событий.
Механизм переключения
- Задача A выполняется, пока не достигнет точки переключения (например, вызов
yield()). - Планировщик сохраняет контекст задачи A (регистры, стек, счётчик команд) в специальную структуру.
- Планировщик выбирает следующую задачу B из очереди готовых к выполнению.
- Восстанавливается контекст задачи B, и выполнение продолжается с места, где она была прервана.
- Если задача B не вызывает
yield()и не завершается, она монопольно занимает процессор, блокируя все остальные задачи.
Особенности реализации
- Однопоточность — кооперативная многозадачность часто реализуется в одном потоке операционной системы, что упрощает синхронизацию (нет гонок данных), но не позволяет использовать многоядерные процессоры для параллельного выполнения задач.
- Неблокирующий ввод-вывод — чтобы избежать зависания всей системы, задачи должны использовать неблокирующие операции ввода-вывода (например,
select(),epoll(),kqueue()) или асинхронные вызовы. - Стек задач — каждая задача может иметь собственный стек (в случае корутин — обычно небольшой, до нескольких килобайт), что позволяет эффективно управлять памятью.
Сравнение с вытесняющей многозадачностью
| Характеристика | Кооперативная многозадачность | Вытесняющая многозадачность |
|---|---|---|
| Управление переключением | Добровольное (задача сама решает, когда уступить) | Принудительное (планировщик использует таймеры и прерывания) |
| Сложность реализации | Низкая (не требуется аппаратная поддержка прерываний) | Высокая (требуется управление прерываниями, защита контекста) |
| Надёжность | Низкая (одна зависшая задача блокирует всю систему) | Высокая (зависшая задача может быть принудительно завершена) |
| Производительность | Высокая (нет накладных расходов на частые прерывания) | Средняя (накладные расходы на переключение контекста по таймеру) |
| Синхронизация | Простая (нет гонок данных в одном потоке) | Сложная (требуются мьютексы, семафоры, критические секции) |
| Использование многоядерности | Ограниченное (обычно один поток ОС) | Полное (возможно параллельное выполнение на разных ядрах) |
| Примеры | Mac OS 9, Windows 3.x, Python asyncio, Node.js | Windows 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 →