Мы с Лёшей Большаковым спроектировали и реализовали модуль для кооперативной многозадачности. Вот пример с летающими шариками, тут никакой синхронизации, всё дёргается) Но фишка не в этом. Все шарики двигаются внутри вот этой одной процедуры, выполняемой параллельно 8 раз:
Код:
PROCEDURE MoveBall;
VAR
x, y: INTEGER; sx, sy: SHORTINT;
BEGIN
x := b.RND(20, 240); y := b.RND(20, 160);
sx := SHORT( b.RND(0, 1) ); IF sx = 0 THEN sx := -1 END;
sy := SHORT( b.RND(0, 1) ); IF sy = 0 THEN sy := -1 END;
g.PUTSPR(x, y, 1, 8, SYSTEM.ADR(Ball), g.XORSPR);
LOOP
g.PUTSPR(x, y, 1, 8, SYSTEM.ADR(Ball), g.XORSPR);
IF (x < 1) OR (x > 247) THEN sx := -sx END;
x := x + sx;
IF (y < 8) OR (y > 190) THEN sy := -sy END;
y := y + sy;
g.PUTSPR(x, y, 1, 8, SYSTEM.ADR(Ball), g.XORSPR);
t.Yield;
END;
END MoveBall;
PUTSPR - процедура Сержа Колотова для вывода спрайта с пиксельной точностью. Я её не модифицировал для работы с буфером. И критики по поводу дёрганья шариков не жду. Именно для шариков лучше было бы делать всё в цикле. Это я так, для теста. Модуль Tasks и примеры его использования залиты в репозиторий.
Переключение контекста задач очень легковесное, буквально несколько машинных команд.
Вот документация на модуль:
Код:
MODULE Tasks; (* non-portable *)
(*
Модуль обрабатывает следующие нештатные ситуации:
22 "Statement lost" - Run запущен не из "главной" задачи
25 "Parameter error" - эта задача уже есть в списке
*)
Каждая задача обладает своим собственным стеком и может иметь локальные переменные. Поэтому ей требуется своя память для работы. Для этого статически или динамически создаётся переменная, которая хранит память для задачи. Представлены следующие модели задач, отличающиеся размером стека:
Код:
TYPE
Low = RECORD (Context) stack: ARRAY 20 OF BYTE END;
Tiny = RECORD (Context) stack: ARRAY 40 OF BYTE END;
Small = RECORD (Context) stack: ARRAY 60 OF BYTE END;
Medium = RECORD (Context) stack: ARRAY 80 OF BYTE END;
Large = RECORD (Context) stack: ARRAY 100 OF BYTE END;
Huge = RECORD (Context) stack: ARRAY 120 OF BYTE END;
Модель Tiny имеет размер стека 40 байтов, и этого достаточно для обработки прерывания IM 1. Модель Low гарантированно упадёт при обработке прерывания IM 1 и нужна для особых случаев (работа в режиме DI).
Код:
PROCEDURE Count (): SHORTINT;
Возвращает количество запущенных задач. В коде есть ограничение на предел количества задач - 255. Ограничение видится разумным, т.к. при большом количестве задач тратится всё больше ресурсов на переключение их контекста. Плюс ещё и память. Модель Tiny берёт 50 байт на стек задачи. 50x255 = 12750.
Код:
PROCEDURE Spawn ((*VAR*) ctx: Context; proc: PROCEDURE);
Ставит задачу proc в очередь выполнения. Выполнение начнётся по кольцу при вызове процедуры Run.
Код:
PROCEDURE Id (ctx: Context): INTEGER;
Возвращает id (идентификационный уникальный номер) контекста - двухбайтовое целое число.
Код:
PROCEDURE MyId (): INTEGER;
Возвращает свой id, т.е. текущей выполняемой задачи. В случае основной задачи он равен 0.
Выполняет атом одной из задач в кольце. Нормальное использование Run - вызывать в цикле, пока есть активные задачи: REPEAT Tasks.Run UNTIL Tasks.Count() = 0
Run спроектирован таким образом, чтобы после каждого атома (Yield) отдавать управление основной задаче. Это может понадобиться для выполнения в основной задаче каких-то действий с более высоким приоритетом.
Своеобразный "разрыв". Вызывается внутри задачи для передачи управления другим задачам.
Смотрите примеры работы с модулем Tasks: TestTasks и MoveBalls.