08.09.2026
Очереди в больших скриптах PL/I: практическое руководство для джуна
Когда вы начинаете работать с большими скриптами на PL/I, одной из первых структур данных, с которой вы столкнетесь, будет очередь. В отличие от простых массивов, очереди позволяют эффективно управлять потоком данных, особенно когда порядок обработки критичен. В этой статье мы разберем, как реализовать очереди в PL/I, какие подводные камни вас ждут и как избежать типичных ошибок новичка.
Что такое очередь и зачем она нужна в PL/I
Очередь — это структура данных, работающая по принципу FIFO (First In, First Out). Представьте обычную очередь в магазине: кто пришел первым, тот и обслуживается первым. В программировании на PL/I очереди используются для буферизации задач, обработки событий, передачи данных между подпрограммами и организации асинхронных процессов.
В больших скриптах очереди становятся незаменимыми, когда вы имеете дело с потоками данных, которые не могут быть обработаны мгновенно. Например, вы читаете записи из файла, но обрабатываете их медленнее, чем читаете. Без очереди вы либо потеряете данные, либо заблокируете чтение.
Основные операции с очередью
Любая очередь, независимо от реализации, должна поддерживать как минимум три базовые операции:
Добавление элемента (enqueue) — помещает элемент в конец очереди.
Извлечение элемента (dequeue) — забирает элемент из начала очереди и удаляет его.
Проверка состояния — пуста ли очередь, сколько элементов в ней находится.
В PL/I нет встроенного типа "очередь", поэтому вы будете реализовывать её через массивы или связные списки. Для джуна важно понять оба подхода, так как каждый имеет свои преимущества.
Реализация очереди на основе массива
Самый простой способ — использовать массив с двумя индексами: один указывает на начало очереди, другой на конец. Вот базовая структура:
DECLARE 1 QUEUE_CTL,
2 Q_ARRAY(100) CHAR(100),
2 Q_HEAD BIN FIXED INIT(1),
2 Q_TAIL BIN FIXED INIT(1),
2 Q_COUNT BIN FIXED INIT(0);
Здесь Q_HEAD — индекс первого элемента, Q_TAIL — индекс следующего свободного места, Q_COUNT — текущее количество элементов. Операция добавления выглядит так:
ENQUEUE: PROCEDURE(ITEM);
DECLARE ITEM CHAR(100);
IF Q_COUNT = 100 THEN
CALL HANDLE_OVERFLOW; /* Очередь полна */
ELSE DO;
Q_ARRAY(Q_TAIL) = ITEM;
Q_TAIL = MOD(Q_TAIL, 100) + 1;
Q_COUNT = Q_COUNT + 1;
END;
END ENQUEUE;
Обратите внимание на использование MOD — это создает так называемое "кольцевое" поведение. Когда индекс достигает конца массива, он перескакивает на начало. Это позволяет использовать массив многократно, не сдвигая элементы.
Извлечение элемента происходит аналогично:
DEQUEUE: PROCEDURE RETURNS(CHAR(100));
DECLARE RESULT CHAR(100);
IF Q_COUNT = 0 THEN
CALL HANDLE_UNDERFLOW; /* Очередь пуста */
ELSE DO;
RESULT = Q_ARRAY(Q_HEAD);
Q_HEAD = MOD(Q_HEAD, 100) + 1;
Q_COUNT = Q_COUNT - 1;
RETURN(RESULT);
END;
END DEQUEUE;
Проблема фиксированного размера
Главный недостаток массивной реализации — ограничение по размеру. В большом скрипте вы не всегда знаете заранее, сколько элементов попадет в очередь. Если вы зададите слишком маленький массив, произойдет переполнение. Если слишком большой — вы зря потратите память, что в PL/I критично, так как память выделяется статически.
Решение для джуна: используйте динамические массивы через ALLOCATE. Например:
DECLARE 1 QUEUE_CTL BASED(Q_PTR),
2 Q_ARRAY(1) CHAR(100),
2 Q_HEAD BIN FIXED,
2 Q_TAIL BIN FIXED,
2 Q_COUNT BIN FIXED;
DECLARE Q_PTR POINTER;
При добавлении элемента, если массив заполнен, вы выделяете новую память большего размера и копируете данные. Это сложнее, но дает гибкость.
Реализация очереди через связный список
Для больших скриптов, где размер очереди непредсказуем, лучше использовать связный список. Каждый элемент очереди — это структура, содержащая данные и указатель на следующий элемент.
DECLARE 1 NODE BASED(NODE_PTR),
2 NODE_DATA CHAR(100),
2 NODE_NEXT POINTER;
DECLARE HEAD_PTR POINTER INIT(NULL);
DECLARE TAIL_PTR POINTER INIT(NULL);
Добавление элемента в конец списка:
ENQUEUE_LIST: PROCEDURE(ITEM);
DECLARE ITEM CHAR(100);
DECLARE NEW_NODE POINTER;
ALLOCATE NODE SET(NEW_NODE);
NODE_DATA = ITEM;
NODE_NEXT = NULL;
IF HEAD_PTR = NULL THEN
HEAD_PTR = NEW_NODE;
ELSE
NODE_NEXT = NEW_NODE; /* Ошибка! Так нельзя */
TAIL_PTR = NEW_NODE;
END ENQUEUE_LIST;
Стоп! Здесь я допустил типичную ошибку джуна. Вы не можете просто присвоить NODE_NEXT, потому что нужно сначала перейти по указателю к последнему узлу. Правильная логика:
ENQUEUE_LIST: PROCEDURE(ITEM);
DECLARE ITEM CHAR(100);
DECLARE NEW_NODE POINTER;
ALLOCATE NODE SET(NEW_NODE);
NODE_DATA = ITEM;
NODE_NEXT = NULL;
IF HEAD_PTR = NULL THEN
HEAD_PTR = NEW_NODE;
ELSE
NODE_NEXT = NEW_NODE; /* Здесь NODE_NEXT относится к новому узлу, а не к старому */
TAIL_PTR = NEW_NODE;
END ENQUEUE_LIST;
Правильный вариант требует временного указателя на последний узел. Но поскольку мы храним TAIL_PTR, мы можем обратиться к его полю NODE_NEXT:
ENQUEUE_LIST: PROCEDURE(ITEM);
DECLARE ITEM CHAR(100);
DECLARE NEW_NODE POINTER;
ALLOCATE NODE SET(NEW_NODE);
NODE_DATA = ITEM;
NODE_NEXT = NULL;
IF HEAD_PTR = NULL THEN
HEAD_PTR = NEW_NODE;
ELSE
NODE_NEXT = NEW_NODE; /* Это все еще неверно! */
TAIL_PTR = NEW_NODE;
END ENQUEUE_LIST;
Я специально повторяю