Потоки, события и ожидание результата: многозадачность в PL/I

Когда говорят о PL/I, воображение часто рисует большие машины и программы из другой эпохи. Но у языка есть и вполне современная сторона: процедуры, массивы, многопоточное выполнение. Если вы писали на Go или Python, базовый сценарий будет понятен: запустить работу в фоне, пока она идёт заняться чем‑то ещё, а затем дождаться результата.
Разберём этот сценарий на примерах, которые опираются на реализованную многозадачность в pli‑llvm — новом открытом кроссплатформенном компиляторе языка PL/I, который был создан на основе спецификации языка TR25 с добавлением некоторых более поздних полезных фишек из IBM Enterprise PL/I.
Запустить задачу и дождаться её завершения
В Go для этого обычно используют горутину и sync.WaitGroup, в Python — поток и join(). В PL/I аналогичную задачу выполняют call … event(…) и wait(…):
demo: proc options(main);
dcl done event;
dcl counter fixed bin(31) init(0);
call worker event(done);
put skip list(‘main continues’);
wait(done);
put skip list(‘counter =’, counter);
worker: proc;
counter = counter + 1;
end;
end demo;call запускает процедуру worker в фоновом потоке, а wait(done) приостанавливает главный поток до завершения задачи. После этого программа читает значение counter. Для сравнения — аналогичный код на Go:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
wg.Wait()В Python с модулем threading:
thread = threading.Thread(target=worker)
thread.start()
thread.join()Синтаксис различный, но порядок действий один: запустить независимую работу, заняться своим делом и синхронизироваться перед использованием результата.
Две независимые задачи
Допустим, что нужно посчитать две части набора данных. Запустим для каждой отдельную задачу, а пока они работают — выполним отдельную небольшую задачу в главном потоке:
concurrency: proc options(main);
dcl (left_done, right_done) event;
dcl left_sum fixed bin(63) init(0),
right_sum fixed bin(63) init(0);
dcl foreground fixed bin(31) init(0);
dcl i fixed bin(31);
/* запуск подзадач */
call sum_left event(left_done);
call sum_right event(right_done);
/* параллельное выполнение задачи в основном потоке */
do i = 1 to 100;
foreground = foreground + i;
end;
wait(left_done, right_done);
put skip list('left =', left_sum);
put skip list('right =', right_sum);
put skip list('foreground =', foreground);
sum_left: proc;
dcl i fixed bin(31);
do i = 1 to 100000;
left_sum = left_sum + i;
end;
end;
sum_right: proc;
dcl i fixed bin(31);
do i = 100001 to 200000;
right_sum = right_sum + i;
end;
end;
end;Для каждой задачи есть своё событие: left_done и right_done. Вызов wait с обоими событиями ждёт завершения обеих процедур. Каждая из них записывает в собственную переменную результата — общего изменяемого счётчика здесь нет.
Когда нужен результат — проверить или подождать
Обычно проще вызвать wait, но иногда нужно периодически проверять, закончилась ли задача. Встроенная функция event возвращает '1’b (соответствует true), когда событие завершено, delay(n) задает паузу на n миллисекунд:
do while (^event(done));
put skip list('still working...');
delay(50);
end;Так цикл не проверяет состояние непрерывно и не тратит процессор впустую. Для групп событий предусмотрена и форма wait(events)(k): она ждёт завершения любых k событий из списка или массива.
Несколько деталей, о которых стоит помнить
В данной реализации PL/I асинхронный call выполняется в фоновом потоке. Событие можно объявить отдельно, элементом массива или полем структуры. Поддерживаются события с постоянными границами массива; динамические размеры для этой формы пока что не реализованы.
Событие должно оставаться доступным, пока задача выполняется, поэтому нужно дожидаться её завершения до выхода из блока, где объявлено событие. А если несколько задач записывают в одну переменную, потребуется отдельно продумывать синхронизацию: механизм событий сам по себе не реализовывает безопасный параллельный доступ к данным.
Запуск и компиляция
Компилятор является самодостаточным исполняемым файлом, поэтому достаточно скачать релиз под свою платформу с GitHub, распаковать и добавить в переменную PATH, затем скомпилировать код из примера и запустить:
plic concurrency.pli -o concurrency
./concurrencyРезультат выполнения:
left = 5000050000
right = 15000050000
foreground = 5050Также можно собрать компилятор из исходного кода самостоятельно под свою платформу следую инструкциям в README.
Почему PL/I
Язык PL/I интересен тем, что сочетает скорость C с высокоуровневыми фишками современных языков типа Go и Python — такими, как встроенная многозадачность, сигналы и управление памятью на уровне семантики языка. При этом он не имеет зарезервированных слов, что дает свободу в выборе имен переменных и функций, а дизайн языка делает код легкочитаемым и позволяет писать быстрые программы для высокопроизводительных вычислений.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.