Потоки, события и ожидание результата: многозадачность в 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.WaitGroupwg.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 = 5000050000right = 15000050000foreground = 5050

Также можно собрать компилятор из исходного кода самостоятельно под свою платформу следую инструкциям в README.

Почему PL/I

Язык PL/I интересен тем, что сочетает скорость C с высокоуровневыми фишками современных языков типа Go и Python — такими, как встроенная многозадачность, сигналы и управление памятью на уровне семантики языка. При этом он не имеет зарезервированных слов, что дает свободу в выборе имен переменных и функций, а дизайн языка делает код легкочитаемым и позволяет писать быстрые программы для высокопроизводительных вычислений.

ссылка на оригинал статьи https://habr.com/ru/articles/1091430/