Когда говорят о 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 — такими, как встроенная многозадачность, сигналы и управление памятью на уровне семантики языка. При этом он не имеет зарезервированных слов, что дает свободу в выборе имен переменных и функций, а дизайн языка делает код легкочитаемым и позволяет писать быстрые программы для высокопроизводительных вычислений.

