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