# Стандартная библиотека

# Функция eval(expr: String, initialEnvironment: Object = Empty): Any (вычислить)

Функция позволяет выполнить произвольный скрипт expr, переданный в виде строки. Если в процессе выполнения не возникло исключений, то функция возвращает результат вычисления последнего выражения, иначе - генерируется исключение. Параметр initialEnvironment управляет областью видимости, в которой выполняется скрипт. Если он не задан, то expr исполняется в новой локальной области видимости, связанной с текущей. Таким образом, в скрипте будут доступны все переменные, которые доступны в месте вызова функции eval, а все переменные окружения, созданные в скрипте, будут доступны после его завершения. Если же initialEnvironment является объектом (Object), для выполнения скрипта создается отдельный стек областей видимости, никак не связаный с текущим, и исполнение начинается в новом окружении. При этом все записи из объекта initialEnvironment копируются в новое окружение (становятся глобальными переменными); копирование не глубокое, т.е. непримитивные значения передаются по ссылке. По завершении скрипта этот стек областей видимости уничтожается.

Примеры:

eval(`1 + 1`)                    # возвращает результат вычисления
                                 # выражения, т.е. 2

x = 2
eval(`x + x`)                    # выражение вычисляется в текущем
                                 # окружении, т.е. в нем доступна
                                 # переменная x

try
{
   eval(`throw "Error"`)
}
catch (e)
{
   e.thrown                      # строка "Error"; исключение,
                                 # возникшее в процессе выполнения
                                 # скрипта, можно перехватить в
                                 # коде, вызывавшем eval
}

eval(`x = y + 4`, @{ y: 6 })     # возвращает число 10; значение
                                 # переменной y было скопировано в
                                 # новое окружение из initialEnvironment

x                                # число 2; предыдущее выражение было
                                 # выполнено в отдельном окружении,
                                 # поэтому значение переменной x в
                                 # текущем окружении не изменилось

b = @{ x: 1 }
eval(`b.x = 2`, @{ b })

b.x                              # число 2; при передаче в новый
                                 # контекст выполняется неглубокое
                                 # копирование, т.е. переменные b
                                 # внутри и вне eval ссылаются на
                                 # один и тот же объект

# Модуль ComObject (КомОбъект)

Модуль позволяет работать с Windows COM: создавать экземпляры COM-объектов, вызывать их методы, считывать и записывать свойства, а также обрабатывать события.

# Работа с COM-объектами

Чтобы создать COM-объект, нужно вызвать функцию ComObject (КомОбъект) с одним аргументом - именем нужного объекта.

var xmlObj = ComObject(`Msxml2.DOMDocument.6.0`)

Функция возвращает объект типа IDispatch. Этот объект похож на другие встроенные объекты, однако все обращения к нему (за исключением методов и свойств, имя которых начинается с @) "пробрасываются" в COM-объект.

xml.async                  # true, транслируется в чтение свойства "async" COM-объекта
xml.async = false          # транслируется в запись свойства "async" COM-объекта
xml.async                  # false, значение было изменено в предыдущей строке
xml.loadXml(`<tag />`)     # true, транслируется в вызов метода "loadXml" COM-объекта
                           # метод возвращает значение, которое вернул COM-объект

Для обеспечения совместимости, к свойствам объекта можно обращаться с помощью синтаксиса вызова метода:

xml.async() # то же самое, что и xml.async

Если запрошенное свойство или метод отсутствуют в COM-объекте, возникает исключение. Нативные исключения, которые генерирует COM-объект, можно обработать с помощью try-catch. COM-объект может быть пустым (содержать в себе null), в этом случае любое обращение к нему будет приводить к исключению. Проверить, что COM-объект является пустым, можно с помощью свойства @empty (@пустой):

o.@empty                   # true, если объект пустой
                           # false в противном случае

либо преобразовать его к Boolean. Непустой объект преобразуется в true, пустой - в false:

if (o)
{
# объект не пустой
}
else
{
# объект пустой
}

При передаче значений в COM-объект и обратно автоматически выполняется конвертация в нативный тип VARIANT. Поддерживаются следующие преобразования значений:

Nil         -> VT_NULL
Boolean     -> VT_BOOLEAN
Int32       -> VT_I4
Int64       -> VT_I8
Real        -> VT_R8
Money       -> VT_CY
Date        -> VT_DATE
DateTime    -> VT_DATE
String      -> VT_BSTR
IDispatch   -> VT_DISPATCH
Bytes       -> VT_ARRAY | VT_UI1
Array       -> VT_ARRAY | VT_VARIANT

При конвертации DateTime отбрасываются миллисекунды. Тип Array конвертируется в одномерный или многомерный SAFEARRAY со значениями типа VARIANT, размерность определяется по значениям элементов Array. Если в многомерном Array вложенные массивы имеют разную длину, то в SAFEARRAY пустые ячейки заполняются значением VT_EMPTY. Если многомерный Array сформирован некорректно (на одном уровне присутствуют массивы и не массивы), возникает исключение. Примеры:

@[1, 2]           # OK, одномерный SAFEARRAY
@[[1], [2, 4]]    # OK, двумерный SAFEARRAY; в ячейке [0, 1] будет VT_EMPTY
@[1, [2, 3]]      # исключение, некорректно сформированный двумерный массив

VARIANT конвертируется в значение следующим образом:

VT_EMPTY                -> Empty
VT_NULL                 -> Nil
VT_BOOL                 -> Boolean
VT_I1                   -> Int32
VT_I2                   -> Int32
VT_I4                   -> Int32
VT_I8                   -> Int64
VT_UI1                  -> Int32
VT_UI2                  -> Int32
VT_UI4                  -> Int64
VT_UI8                  -> Int64
VT_INT                  -> Int32
VT_UINT                 -> Int64
VT_ERROR                -> Int32
VT_R4                   -> Real
VT_R8                   -> Real
VT_CY                   -> Money
VT_DATE                 -> DateTime
VT_BSTR                 -> String
VT_DISPATCH             -> IDispatch
VT_ARRAY | VT_UI1       -> Bytes
VT_ARRAY | VT_VARIANT   -> Array

Если VARIANT c типом VT_UI8 имеет значение, которое выходит за пределы диапазона Int64, возникает исключение. VARIANT с признаком VT_BYREF автоматически разыменовывается.

Если конвертацию в VARIANT или обратное преобразование выполнить невозможно, возникает исключение.

Если при вызове метода в качестве аргумента указать имя переменной, начинающееся с префикса out_, то этот аргумент будет передан по ссылке, и вызываемая функция может поменять значение переменной.

# Обработка событий

Подписаться на события COM-объекта можно с помощью метода:

IDispatch::@subscribe(event: String, onEvent: String, onError: String = ""): Real
  • event - имя события

  • onEvent - текст обработчика события

  • onError - текст обработчика ошибки

Псевдоним метода - @подписаться. Если объект не имеет события с именем event, возникает исключение. Метод возвращает идентификатор обработчика, с помощью которого можно в дальнейшем отписаться от события.

Обработка события происходит следующим образом. При возникновении события в окружении создаются переменные:

  • source: IDispatch (источник) - тот же объект, на котором был вызван метод @subscribe

  • arguments: Array (аргументы) - содержит все аргументы, полученные вместе с событием

Далее выполняется фрагмент кода, переданный в аргументе onEvent. Если в процессе выполнения onEvent возникло исключение, либо не удалось выполнить конвертацию аргументов события, в окружении создается переменная error: Any (ошибка), содержащая объект исключения, и выполняется код onError. Если вызов onError также привел к исключению, это исключение игнорируется.

Результат выполнения последнего выражения в onEvent (либо в onError) конвертируется в HRESULT и становится результатом выполнения обработчика. В HRESULT могут быть сконвертированы значения следующих типов:

  • Int32

  • Int64

  • Real

  • Empty

Если значение Real или Int64 выходит за пределы диапазона допустимых для HRESULT значений, возникает исключение. Empty конвертируется в S_OK. Если оба фрагмента (onEvent и onError) вызвали исключение, то результатом выполнения обработчика становится E_FAIL.

Фрагменты onEvent и onError выполняются изолировано от того кода, в котором была выполнена подписка на событие ("основного" кода). Кроме того, onEvent и onError выполняются изолировано друг от друга. Это значит, что в onEvent и onError недоступны никакие переменные из "основного" кода, а также в onError недоступны переменные source и arguments. Переменные, созданные внутри обработчиков, недоступны из "основного" кода. Взаимодействовать с "основным" кодом обработчики могут только с помощью нативных функций.

После выполнения onError и onEvent все переменные, которые они создали, уничтожаются. Таким образом, нельзя сохранять состояние между вызовами обработчика.

Чтобы отписаться от события, нужно вызвать метод

IDispatch::@unsubscribe(handlerID: Real): Boolean
  • handlerID - идентификатор обработчика, который вернул метод @subscribe

После вызова метода @unsubscribe обработчик еще некоторое время может быть вызван. Метод возвращает

  • true, если обработчик с указанным идентификатором был отключен

  • false, если нет обработчика с указанным идентификатором

Примеры:

# предполагаем, что определены функции handleEvent и logError
# внутри onEvent и onError недоступны никакие "внешние" переменные
# (в т.ч. из окружения)

var onEvent = `
   s = .source;
   a = .arguments;
   handleEvent(a, s);
`
var onError = `
   logError(.error);
`

var obj = ComObject(`Msxml2.DOMDocument.6.0`);
var hid = obj.@subscribe(`onReadyStateChange`, onEvent, onError);

# в процессе вызова метода loadXml произойдет событие
# "onReadyStateChange" и будет выполнен фрагмент onEvent,
# который вызовет функцию handleEvent; поскольку выполнение
# onEvent не привело к исключению, onError выполняться не
# будет; результатом выполнения обработчика станет значение,
# которое вернулось из handleEvent
obj.loadXml(`<root />`);

# после выполнения onEvent переменные a и s будут уничтожены;
# при следующем вызове обработчика они будут созданы заново,
# и затем вновь уничтожены; если бы вызов функции handleEvent
# привел к исключению, в onError переменные a и s не были бы
# определены

obj.@unsubscribe(hid); # true

# после отписки от события обработчик onEvent вызываться не будет
obj.loadXml(`<root />`);

# Метод @invoke (@вызвать)

Для работы с COM-объектом помимо прямого обращения к свойствам и вызова методов можно использовать низкоуровневый метод @invoke (@вызвать). Он позволяет выполнять с объектом манипуляции, которые запрещены синтаксисом языка (например, вызвать геттер свойства с аргументами). Метод имеет сигнатуру:

IDispatch::invoke(action: String, attribute: String, ...): Any

Здесь:

  • action - действие, которое нужно выполнить:

    • "get" - получить значение свойства

    • "put" - установить значение свойства

    • "method" - вызвать метод

  • attribute - имя свойства или метода

Все остальные аргументы конвертируются в VARIANT и передаются в качестве аргументов для вызова метода или получения/установки свойства. Пример:

xmlObj.@invoke(`put`, `async`, false); # установить значение свойства async в false

# Модуль JSON

Модуль предназначен для сериализации и десериализации значений в формате JSON.

Сериализация выполняется по правилам:

Nil                              -> JsonNull
Boolean                          -> JsonBoolean
Int32, Real, Money               -> JsonNumber
String                           -> JsonString
Bytes                            -> JsonString
Int64, Time, Date, DateTime      -> JsonString
Array, Set, Struct               -> JsonArray
Object, Instance                 -> JsonObject

Остальные типы данных либо вызывают ошибку сериализации, либо преобразуются в JsonNull.

  • Int64 конвертируется в строку, которая представляет это число

  • Time конвертируется в строку в формате hh:mm:ss

  • Date и DateTime конвертируются в строку в формате ISO 8601: YYYY-MM-DDThh:mm:ss.milZ, причем для Date часы, минуты, секунды и миллисекунды будут равны нулю

  • Bytes конвертируется в строку в кодировке Base64

  • при конвертации Set порядок элементов не определен, при конвертации Struct порядок элементов соответствует порядку полей формата

  • при конвертации Object ключи будут упорядочены лексикографически

Десериализация JSON выполняется по правилам:

JsonNull       -> Nil
JsonBoolean    -> Boolean
JsonNumber     -> Real или String
JsonString     -> String
JsonArray      -> Array
JsonObject     -> Object

Преобразование типов при десериализации не выполняется. Таким образом, если сериализовать в JSON, например, Date или Int64, а потом выполнить обратное преобразование, то результатом будет строка. JsonNumber конвертируется в Real, если его значение входит в диапазон значений этого типа данных, иначе - в String.

Модуль предоставляет следующие функции:

# JSON::parse(str: String): Any (прочитать)

Десериализует str в кодировке UTF-8 в значение. Если str не является валидным JSON, возникает исключение. Пример:

JSON.parse(`true`)                           # true
JSON.parse(`123`)                            # 123
JSON.parse(`{"x": 1, "y": 2}`)               # @{x: 1, y: 2}
JSON.parse(`[1, 2]`)                         # @[1, 2]
JSON.parse(`{"x":`)                          # исключение

# JSON::stringify(value: Any, pretty: Boolean = false): String (записать)

Сериализует value в формате JSON. Аргумент pretty управляет форматированием JSON:

  • false - без форматирования (в одну строку без пробелов)

  • true - с форматированием (человекочитабельное представление)

Если значение невозможно сериализовать в JSON, то оно заменяется на JsonNull. Если value является циклическим графом, возникает исключение. Пример:

JSON.stringify(true)                         # `true`
JSON.stringify(@[1, 2])                      # `[1, 2]`
JSON.stringify(@{x: 1, y: @(z: 2)})          # `{"x":1.0,"y":null}`, HashMap не
                                             # может быть сериализован в JSON

var obj = @{}
obj.x = obj
JSON.stringify(obj)                          # исключение, obj является
                                             # циклическим графом

# JSON::stringifyStrict(value: Any, pretty: Boolean = false): String (записатьСтрого)

Аналогичен stringify. Отличается тем, что генерирует исключение, если значение невозможно сериализовать в JSON, а не заменяет его на JsonNull. Пример:

JSON.stringify(@())           # "null"
JSON.stringifyStrict(@())     # исключение

# Модуль Base64

Модуль предоставляет функции для сериализации и десериализации двоичных данных в формате Base64.

# Base64::encode(b: Any): String (закодировать)

Преобразует последовательность байт в строку в формате Base64. Аргумент b может иметь тип String или Bytes; если передать аргумент другого типа, возникнет исключение. Пример:

Base64.encode(`abc`);                  # "YWJj"
Base64.encode(Bytes(97, 98, 99));      # "YWJj"

# Base64::decode(s: Any): Bytes (декодировать)

Преобразует строку в формате Base64 в последовательность байт. Аргумент s может иметь тип String или Bytes; если передать аргумент другого типа, возникнет исключение. Если строка содержит символы, недопустимые в кодировке Base64, также возникнет исключение. Пример:

Base64.decode(`YWJj`);                 # Bytes(97, 98, 99)
Base64.decode(Bytes(`YWJj`));          # Bytes(97, 98, 99)

# Модуль Console (Консоль)

Модуль предоставляет различные функции для вывода информации в консоль отладчика. Все функции работают аналогично функциям консоли веб-браузера. Если отладчик не подключен, эти функции ничего не делают.

# Console::log(...): Empty (лог)

Печатает свои аргументы в консоли отладчика, уровень сообщения LOG. Пример:

console.log("x = %s", "str");                # напечатает x = str
console.log(1, 2);                           # напечатает 1 2

# Console::info(...): Empty (инфо)

Печатает свои аргументы в консоли отладчика, уровень сообщения INFO. Пример:

console.info("x = %s", "str");               # напечатает x = str
console.info(1, 2);                          # напечатает 1 2

# Console::warning(...): Empty (предупреждение)

Печатает свои аргументы в консоли отладчика, уровень сообщения WARNING. Пример:

console.warning("x = %s", "str");            # напечатает x = str
console.warning(1, 2);                       # напечатает 1 2

# Console::error(...): Empty (ошибка)

Печатает свои аргументы в консоли отладчика, уровень сообщения ERROR. Пример:

console.error("x = %s", "str");              # напечатает x = str
console.error(1, 2);                         # напечатает 1 2

# Console::trace(): Empty (стекВызовов)

Печатает в консоли текущий стек вызовов. Пример:

console.trace(); # напечатает at <top-level> (<anonymous>:1:14)

# Console::assert(...): Empty (проверить)

При вызове без аргументов печатает в консоли сообщение об ошибке.

При вызове с одним и более аргументами, преобразует значение первого аргумента к Boolean. Если оно равно false или не может быть преобразовано в Boolean, печатает в консоли сообщение об ошибке, в которое включаются все остальные аргументы функции, кроме первого. Иначе не делает ничего. Пример:

console.assert(true, "message");          # значение первого аргумента true -
                                          # ничего не делает

console.assert(false, "message");         # значение первого аргумента false -
                                          # печатает в консоли сообщение об ошибке,
                                          # в которое будет включен текст message

console.assert(@{}, "message");           # значение первого аргумента не приводится
                                          # к Boolean - аналогично предыдущему случаю

# Console::clear(): Empty (очистить)

Убирает из консоли всю информацию, которая была выведена в нее ранее.

# Console::group(): Empty (группа)

Группирует вместе все выводимые в консоль сообщения вплоть до вызова функции Console::groupEnd.

# Console::groupEnd(): Empty (закрытьГруппу)

Прекращает группировать сообщения в консоли (т.е. закрывает группу сообщений, которая была создана с помощью Console::group). Если ранее не была вызвана функция Console::group, то ничего не делает.

# Console::time(name: String): Empty (таймер)

Запускает таймер с именем name. Отсчет времени производится в миллисекундах начиная с момента вызова функции. Чтобы остановить таймер и узнать, сколько времени прошло, нужно вызвать Console::timeEnd.

# Console::timeEnd(name: String): Empty (остановитьТаймер)

Останавливает таймер с именем name (запущенный функцией Console::time) и печатает количество миллисекунд, прошедшее с момента его запуска. После остановки значение таймера сбрасывается, и при повторном запуске отсчет снова начинается с нуля. Если таймер с именем name не был ранее запущен, то в консоль выводится предупреждение.

Пример:

console.time("t");                        # запуск таймера с именем "t"
...                                       # какие-то вычисления
console.timeEnd("t");                     # остановка таймера c именем "t"

                                          # в консоли будет напечатано t: N ms,
                                          # где N - количество миллисекунд,
                                          # прошедшее между вызовами time и
                                          # timeEnd

# Модуль Profiler (Профайлер)

Модуль позволяет получить детальный профиль выполнения участка кода. Перед началом исполнения профилируемого участка кода необходимо вызывать метод startProfiling (начатьПрофилирование), а по окончании - метод stopProfiling (закончитьПрофилирование), который вернет данные профиля. Полученные данные можно отсортировать по нужной колонке с помощью метода sort и затем отформатировать в виде таблицы при помощи метода format. Пример:

Profiler.startProfiling()                    # начать профилирование

# профилируемый участок кода

var profile = Profiler.stopProfiling()       # завершить профилирование и
                                             # получить профиль
profile.sort("selfTime")                     # отсортировать профиль по
                                             # колонке Self

var table = profile.format()                 # отформатировать профиль
                                             # в виде таблицы

Строку table можно отобразить в консоли или записать в файл. Ниже приведен пример профиля:

Function       Self              Total               Count   Location

executeTest     0.46% (588 ms)   55.49% (70308 ms)       2   testScript.prg:15:1
getOrDefault   1.36% (1720 ms)     1.36% (1720 ms)       5   <anonymous>:0:0

В профиле содержатся следующие данные:

  • Function - имя функции

  • Self - собственное время выполнения функции (в процентах от общего времени профилировки и абсолютное значение в миллисекундах)

  • Total - общее время выполнения функции (в процентах от общего времени профилировки и абсолютное значение в миллисекундах)

  • Count - количество вызовов функции

  • Location - расположение функции в формате <файл>:<строка>:<колонка>

Собственное время выполнения функции (self time) - это общее время выполнения функции (total time) минус общее время выполнения вызванных ею функций, т.е. время выполнения инструкций непосредственно в теле функции.

# Методы Profiler

# Profiler::startProfiling(): Empty (начатьПрофилирование)

Запускает процесс записи профиля выполнения участка кода. Если профайлер уже был запущен, то собранные данные уничтожаются и процесс профилирования начинается заново.

# Profiler::stopProfiling(threshold: Real = 0.1): Profile (закончитьПрофилирование)

Завершает процесс записи профиля выполнения участка кода и возвращает собранные данные. Если профайлер не был ранее запущен, вернет пустой профиль без записей. Аргумент threshold задает порог для отбора записей профиля, а именно, в профиль попадут только те функции и методы, у которых собственное или общее время выполнения в процентах больше указанного значения. threshold может принимать значения от 0 до 100 процентов.

# Методы Profile

# Profile::sort(order: String): Empty (сортировать)

Выполняет сортировку записей профиля по указанному критерию order. Параметр order может принимать следующие значения:

  • "function" ("функция") - сортировка по имени функции (по возрастанию)

  • "selfTime" ("собственноеВремя") - сортировка по собственному времени выполнения функции (по убыванию)

  • "totalTime" ("общееВремя") - сортировка по общему времени выполнения функции (по убыванию)

  • "invocationCount" ("количествоВызовов") - сортировка по количеству вызовов функции (по возрастанию)

  • "location" ("расположение") - сортировка по расположению функции в исходном коде (по возрастанию)

# Profile::format(): String (форматировать)

Форматирует данные профиля в виде таблицы.

# Модуль Worker (ПараллельныйПоток)

Модуль позволяет запускать параллельные потоки выполнения кода.

Поток не имеет доступа к окружению, в котором он был создан, обмен данными с ним выполняется через каналы. При передаче через канал данные копируются либо перемещаются в поток (кроме многопоточных объектов, см. ниже). Таким образом потоки не имеют общих данных, доступ к которым нужно синхронизировать, и это упрощает разработку многопоточного кода.

Канал является простой очередью: при записи в канал элементы добавляются в конец очереди, а при чтении - извлекаются из начала. По умолчанию максимальная длина очереди не фиксирована (то есть в очереди может храниться произвольное количество элементов), однако для управления пропускной способностью канала ее можно явно огранчить. Максимальная длина очереди задается при ее создании, после этого ее изменить нельзя.

Операции чтения и записи в канал могут блокировать вызывающий поток. При чтении из канала поток будет заблокирован до появления данных в канале, если его очередь пуста. При записи в канал блокировка происходит тогда, когда канал переполнен (т.е. достигнута максимальная длина очереди); поток будет разблокирован при появлении свободного места в канале. Во всех остальных случаях данные будут сразу же прочитаны/записаны в канал, и управление вернется вызывающему потоку.

При создании потока канал автоматически связывается с ним, а при завершении потока эта связь уничтожается. Если канал не связан ни с одним потоком, то блокирующие операции чтения и записи будут сразу завершаться исключением.

Для упрощения отладки потокам и каналам можно присваивать имена. Эти имена будут видны в отладчике, также их можно узнать с помощью свойства name (имя). По умолчанию потокам и каналам присваивается имя <anonymous>.

Далее в примерах предполагается, что функция sleep останавливает выполнение потока на указанное количество миллисекунд.

# Использование каналов

Канал создается с помощью функции channel (канал), которая возвращает новый объект с типом Channel:

var c = сhannel("n", 2)                   # создает канал с именем "n"
                                          # и размером буфера 2

c.name                                    # "n"
c.maxQueueSize                            # 2

Записать данные в канал можно с помощью метода push (отправить):

var c = сhannel()

c.push("item-1")
c.push("item-2")

# теперь в очереди канала хранятся две строки
# "item-1" и "item-2" в указанном порядке

По достижении максимальной длины очереди метод push заблокирует вызывающий поток до появления места в очереди (либо сгенерирует исключение, если с каналом не связан ни один поток):

var c = сhannel(1)

c.push("item-1")
c.push("item-2")                          # исключение, с каналом не связан
                                          # ни один поток, поэтому попытка
                                          # записи вызовет перманентную
                                          # блокировку текущего потока

Если максимальная длина очереди не ограничена, метод pull никогда не будет приводить к блокировке.

Метод tryPush (попытатьсяОтправить) также записывает данные в канал, но, в отличие от push, позволяет ограничить максимальную длительность блокировки потока в ожидании освобождения очереди. Метод возвращает true, если данные удалось записать в очередь, и false, если истек интервал ожидания:

c.tryPush("item", 100)                    # пытается записать в очередь строку
                                          # "item"; если это получится сделать
                                          # в течении 100 мс, вернет true,
                                          # иначе вернет false

Чтение из канала осуществляется с помощью метода pull (получить):

var c = сhannel()

c.push("item")
c.pull()                                  # возвращает "item"

Метод удаляет из очереди канала первый элемент и возвращает его. Если в канале есть данные, метод pull сразу же возвращает управление, иначе вызывающий поток блокируется до появления данных (либо возникает исключение, если канал не связан ни с одним потоком):

var c = сhannel()

c.pull()                                  # исключение, с каналом не связан
                                          # ни один поток, поэтому попытка
                                          # чтения вызовет перманентную
                                          # блокировку текущего потока

Метод tryPull (попытатьсяПолучить) аналогичен методу pull, но позволяет задать максимальную длительность блокировки потока в ожидании поступления данных. Он принимает имя переменной для записи результата чтения и длительность интервала в миллисекундах, и возвращает true, если удалось прочитать данные, или false, если истек интервал ожидания. Прочитанные данные записываются в локальную переменную с указанным именем; если такой переменной нет, она создается:

c.tryPull(result, 100)                    # пытается прочитать данные из
                                          # очереди; если это удастся сделать
                                          # в течение 100 мс, метод вернет
                                          # true и запишет прочитанные данные
                                          # в переменную result, иначе вернет
                                          # false

При передаче данных через канал они структурно копируются:

  • примитивные типы данных передаются по значению

  • нативные объекты, которые поддерживают многопоточный доступ, передаются по ссылке

  • встроенные коллекции поэлементно копируются, причем ко вложенным в них коллекциям также применяется структурное копирование (т.н. глубокое копирование)

Также можно переместить данные в канал с помощью функции teleport (телепортировать). Переместить данные можно только в том случае, если после перемещения к ним нельзя будет получить доступ извне канала. Например, если на массив ссылаются две переменные, этот массив нельзя будет переместить, поскольку можно будет обратиться к нему через вторую переменную. Аналогично, нельзя будет переместить массив со вложенным массивом, если на вложенный массив ссылается переменная. Заново получить доступ к данным можно будет только прочитав их из канала. Перемещать можно те же типы данных, которые поддерживают структурное копирование, а также перемещаемые нативные объекты.

Функция teleport принимает имя переменной (или ключ в коллекции), считывает ее значение, а затем удаляет эту переменную. Она возвращает значение переменной, подготовленное к перемещению. Результат вызова teleport нужно передать в метод push или tryPush. Если в качестве аргумента функции teleport передано не имя переменной либо данные невозможно переместить, возникает исключение. Также исключение возникает при повторной передаче перемещенных данных в метод push или tryPush.

Остальные типы данных передавать в канал нельзя, это вызывает исключение.

var c = сhannel()
c.push("s")                               # строка передается по значению

var c = сhannel()
var origin = @[1, 2, 3]
c.push(origin)                            # массив origin копируется
var clone = c.pull()
clone[1] = -1                             # массив origin не меняется
                                          # поскольку при записи в канал
                                          # он был скопирован

var c = сhannel()
var array = @[1, 2, 3]
var origin = @{array}
c.push(origin)                            # объект origin копируется
var clone = c.pull()
clone.array[1] = -1                       # массив array не меняется
                                          # поскольку при записи в канал
                                          # он был скопирован

var c = сhannel()
var array = @[1, 2, 3]
c.push(teleport(array))                   # массив array перемещается в канал
                                          # переменная array удаляется
                                          # получить доступ к массиву можно
                                          # только прочитав его из канала

var c = сhannel()
var array = @[1, 2, 3]
var object = @{array}
c.push(teleport(object))                  # исключение, объект object нельзя
                                          # переместить в канал, поскольку
                                          # ко вложенному массиву array можно
                                          # получить доступ через переменную

var c = сhannel()
var array = @[1, 2, 3]
var t = teleport(a)
c.push(t)                                 # массив array перемещен в канал
c.push(t)                                 # исключение, нельзя повторно перемещать
                                          # одни и те же данные

# Использование потоков

Параллельный поток создается функцией worker (параллельныйПоток). Функция принимает текст скрипта, который нужно выполнить в новом потоке. Выполнение скрипта начинается сразу же по выходу из функции worker:

var w = worker(`
   sleep(1000)
   console.log("stop")
`)

# напечатает в консоли stop через одну секунду

Функция возвращает объект типа Worker, который позволяет взаимодействовать с созданным потоком.

По умолчанию вместе с потоком создаются два канала: один для чтения данных из потока, другой - для записи. В потоке, который создал Worker, эти каналы можно получить с помощью свойств объекта input (входнойКанал) и output (выходнойКанал), внутри созданного потока - с помощью переменных окружения output (выходнойКанал) и input (входнойКанал) соответственно. Следует обратить внимание на то, входной канал объекта Worker (т.е. свойство input) является выходным каналом внутри созданного потока (т.е. переменной output), и наоборот. Кроме того, input не имеет методов записи (push и tryPush), а output - методов чтения (pull и tryPull). Пример:

var w = worker(`
   s = input.pull()
   output.push(s + s)
`)
w.output.push("z")
w.input.pull()                            # возвращает "zz"

Для удобства объект Worker имеет следующие методы:

  • pull (эквивалентно input.pull)

  • tryPull (эквивалентно input.tryPull)

  • push (эквивалентно output.push)

  • tryPush (эквивалентно output.tryPush)

Внутри потока доступны аналогичные глобальные функции. С их помощью можно сократить предыдущий пример:

var w = worker(`
   s = pull()
   push(s + s)
`)
w.push("z")
w.pull()                            # возращает "zz"

В создаваемый поток можно передать заранее подготовленные каналы (например, если несколько потоков должны обрабатывать данные с одного источника), они станут входным и выходным каналами нового потока:

var i = channel()
var o = channel()

worker(`console.log(pull())`, i, o)
worker(`console.log(pull())`, i, o)

o.push(1)
o.push(2)

# напечатает в консоли строки 1 и 2 либо 2 и 1 в зависимости
# от того, какой из потоков вызовет метод console.log первым

В случае, когда несколько потоков ожидают получения данных из канала (либо возможности записи в канал), при появлении нового элемента в очереди будет разблокирован только один выбранный случайным образом поток, остальные потоки останутся заблокированными в ожидании новых данных:

var i = channel()
var o = channel()

var w1 = worker(`pull()`, i, o)
var w2 = worker(`pull()`, i, o)

o.push(1)

# после вызова push будет разблокирован либо w1 либо w2

Ожидать завершения созданного потока необязательно: по завершении работы поток будет автоматически уничтожен:

worker(`
   for (var i = 1; i <= 3; i++)
   {
      sleep(1000)
      console.log(i)
   }
`)

# поток напечатает в консоли строки 1, 2 и 3 с интервалом 1 сек
# и затем будет автоматически уничтожен

Если все же необходимо дождаться завершения потока, можно использовать метод join ( ждатьЗавершения), который заблокирует вызывавший его поток до окончания работы другого потока:

var w = worker(`
   sleep(1000)
`)
w.join()

# основной поток будет заблокирован на 1 сек (то есть
# пока не завершится поток w)

Метод interrupt (прервать) прерывает выполнение потока. Он принудительно завершает выполнение скрипта, переданного в функцию worker, однако это происходит с некоторой задержкой. Кроме того, если поток заблокирован в ожидании данных из канала (либо ожидает возможности записи в канал), то он будет прерван только после разблокировки. Поэтому, если предполагается возможность внешнего прерывания потока, для работы с каналом в нем нужно использовать методы tryPull и tryPush, чтобы поток периодически разблокировался. Пример:

var w = worker(`while (true) {}`)
sleep(3000)
w.interrupt()
w.join()

# поток w будет принудительно завершен через 3 сек после запуска

Если поток завершился в результате неперехваченного исключения, получить это исключение можно с помощью свойства uncaughtException (неперехваченноеИсключение). Оно будет равно nil, если поток еще не завершился либо завершился без ошибок, иначе это свойство вернет Object с полями

  • thrown (брошенноеЗначение) - объект исключения

  • trace (стекВызовов) - стек вызовов в том месте, где возникло исключение

Поле trace может отсутствовать (например, если не удалось скомпилировать текст скрипта, и его выполнение даже не начиналось).

var w = worker(`pull(); throw "Error"`)
w.uncaughtException                       # nil, поток еще не завершен

w.push(0)
w.join()
w.uncaughtException.thrown                # строка "Error"

# Функция channel

Создает новый канал. Поддерживаются следующие варианты вызова:

  1. channel(): Channel

  2. channel(name: String): Channel

  3. channel(maxQueueSize: Number): Channel

  4. channel(name: String, maxQueueSize: Number): Channel

Аргумент name задает имя канала, maxQueueSize - максимальную длину очереди канала. По умолчанию в качестве имени канала используется <anonymous>, максимальная длина очереди не ограничивается.

# Методы Channel

# Channel::push(v: Any): Empty (отправить)

Записывает значение v в канал. Блокирует выполнение потока, пока в очереди канала не появится свободное место для записи данных.

# собирает все считанные из канала значения в массив,
# пока не встретится значение nil; после этого возвращает
# набранный массив

var w = worker(`
   r = @[]
   while (true)
   {
      var v = pull()
      if (v != nil)
      {
         r.pushBack(v)
      }
      else
      {
         push(r)
         break
      }
   }
`)

for (var i = 1; i <= 3; i++)
   w.push(i)

w.push(nil)
r = w.pull()                              # @[1, 2, 3]

w.join()

# Channel::tryPush(v: Any, timeout: Number = 0): Boolean (попытатьсяОтправить)

Записывает значение v в канал, если в его очереди появляется свободное место до истечения timeout (в миллисекундах). Возвращает true, если значение удалось записать, иначе - false.

# ограничение нагрузки на фоновый поток

var i = channel()
var o = channel(1)

var w = worker(`
   while (true)
   {
      sleep(1000)
      console.log(pull())
   }
`, i, o)

for (var i = 0; i < 1000; i++)
   while (!w.tryPush(i, 10))
      doOtherWork();

# Channel::pull(): Any (получить)

Считывает значение из канала. Блокирует выполнение потока, пока в очереди канала не появятся данные.

# собирает первые три значения, которые записал поток, в массив

var w = worker(`
   for (var i = 10; i <= 20; i++)
      push(r)
`)

var r = @[]
for (var i = 1; i <= 3; i++)
   r.pushBack(w.pull())

# r содержит числа 11, 12 и 13

# Channel::tryPull(r: ?, timeout: Number = 0): Boolean (попытатьсяПолучить)

Считывает в переменную r значение из канала, если в его очереди появляются данные до истечения timeout (в миллисекундах). Возвращает true, если удалось прочитать данные, иначе - false.

# поток с возможностью принудительного завершения

var w = worker(`
   while (true)
   {
      if (tryPull(v, 100))
         processData(v)
   }
`)

for (var i = 1; i <= 3; i++)
   w.push(i)

w.interrupt()
w.join()

# Свойства Channel

# Channel::name: String (имя)

Возвращает имя канала.

var c = channel(`n`)
c.name                                    # "n"

# Channel::maxQueueSize: Number (максимальныйРазмерОчереди)

Возвращает максимальную длину очереди, либо 0, если он не ограничен.

var c = channel(2)
c.maxQueueSize                            # 2

# Channel::hasUnboundedQueue: Number (размерОчередиНеОграничен)

Возвращает true, если максимальная длина очереди не ограничена.

var c = channel()
c.hasUnboundedQueue                       # true

# Функция worker

Создает параллельный поток выполнения кода. Поддерживаются следующие варианты вызова:

  1. worker(script: String): Worker

  2. worker(script: String, options: Object): Worker

  3. worker(script: String, name: String): Worker

  4. worker(script: String, input: Channel, output: Channel): Worker

  5. worker(script: String, input: Channel, output: Channel, name: String): Worker

В арументе script передается текст скрипта, который будет выполнен в отдельном потоке. input и output являются входным и выходным каналами потока; если их нет, для потока создается собственная пара каналов. Аргумент name задает имя потока, по умолчанию - <anonymous>. Все настройки потока можно передать через объект options, который может иметь следующие ключи:

  • name (имя)

  • input (входнойКанал)

  • output (выходнойКанал)

Если настройка отсутствует в options, используется значение по умолчанию.

Примеры:

# создает поток с именем по умолчанию и собственной парой каналов
worker(`console.log("done")`)

# создает поток, к которому будут привязаны каналы i и o
var i = channel()
var o = channel()
worker(`console.log("done")`, i, o)

# создает поток, к которому будут привязаны каналы i и o
# устанавливает имя потока "w"
var i = channel()
var o = channel()
worker(`console.log("done")`, @{name: "w", input: i, output: o})

# Методы Worker

# Worker::join(): Empty (ждатьЗавершения)

Блокирует выполнение вызвавшего этот метод потока, пока не будет завершен поток Worker.

w = worker(`sleep(5000)`)
w.join()                                  # блокирует выполнение основного
                                          # потока на 5 сек, пока не завершится
                                          # поток w

# Worker::interrupt(): Empty (прервать)

Устаналивает флаг прерывания потока. В процессе выполнения скрипта это флаг периодически проверяется, и как только он станет равен true, поток немедленно завершится. Проверка флага не выполняется, пока поток заблокирован операциями чтения/записи в канал либо другим нативным кодом.

w = worker(`while (true) {}`)
sleep(5000)
w.interrupt()                             # поток w будет завершен примерно
                                          # по истечении 5 сек
w.join()

w = worker(`while (true) { tryPull(x, 5000) }`)
w.interrupt()                             # поток может быть завершен только тогда,
                                          # когда он выйдет из функции tryPull
w.join()

# Worker::pull(): Any (получить)

Аналогично вызову input.pull.

# Worker::tryPull(r: ?, timeout: Number): Any (попытатьсяПолучить)

Аналогично вызову input.tryPull.

# Worker::push(v: Any): Empty (отправить)

Аналогично вызову output.push.

# Worker::tryPush(v: Any, timeout: Number): Empty (попытатьсяОтправить)

Аналогично вызову output.tryPush.

# Свойства Worker

# Worker::script: String (скрипт)

Текст выполняемого скрипта, первый аргумент вызова функции worker.

w = worker(`1 + 2`)
w.script                                  # "1 + 2"

# Worker::name: String (имя)

Возвращает имя потока.

w = worker(`console.log("done")`, `w`)
w.name                                    # "w"

# Worker::input: Channel (входнойКанал)

Возвращает входной канал потока.

i = channel()
w = worker(`console.log("done")`, @{input: i})
w.input                                   # входной канал потока, i

# Worker::output: Channel (выходнойКанал)

Возвращает выходной канал потока.

o = channel()
w = worker(`console.log("done")`, @{output: o})
w.output                                  # выходной канал потока, o

# Worker::active: Boolean (активен)

Возвращает true, если поток еще не завершен, иначе - false.

w = worker(`pull()`)
w.active                                  # true
w.push(nil)
w.join()
w.active                                  # false

# Worker::interrupted: Boolean (прерван)

Возвращает true, если поток был прерван вызовом метода interrupt, иначе - false. Флаг устанавливается сразу после вызова метода interrupt, даже если поток еще не завершен.

w = worker(`while (true) {}`)
w.interrupted                             # false
w.interrupt()
w.interrupted                             # true

# Worker::uncaughtException: Any (неперехваченноеИсключение)

Если поток был завершен в результате неперехваченного исключения, возвращает объект с двумя полями:

  • thrown (брошенноеЗначение) - объект исключения

  • trace (стекВызовов) - стек вызовов на момент возникновения исключения; может отсутствовать

Если поток еще не завершен либо завершился без ошибок, чтение этого свойства возвращает nil.

w = worker(`pull(); throw "Error"`)
w.uncaughtException                       # nil
w.push(nil)
w.join()
w.uncaughtException.thrown                # "Error"