Менеджер контекста
Одиночный оператор захвата ссылки и синхронизации доступа к объекту ‘*’ или ‘*(…)’ выполняется только для одной переменной при использовании в одном действии. Но захват ссылки и объекта синхронизации, это относительно медленная операция и выполнять её для каждого действия над переменной не рационально.
Для того, чтобы захватить одновременно сразу несколько объектов синхронизации для выполнения группы последовательных действий используется менеджер контекста.
**( val1 = *ref1, val2 = *ref2, __timeout__ = 10000 ){
...
};
**( ... ){
...
} , [...] {
# Ветка else при ошибке захвата ссылки
# Информация об ошибке в переменной $^
...
};
Или таже самая запись с использованием макросов DSL:
@with( val1 = *ref1, val2 = *ref2, __timeout__ = 10000 ){
...
};
@with( ... ){
...
} @else {
# Ветка else при ошибке захвата ссылки
# Информация об ошибке в переменной $^
...
};
Захват легких ссылок совместного доступа
При захвате обычной легкой ссылки всегда сравнивается идентификатор текущего потока с идентифкатором потока - владельца объекта. Использование обычных легких ссылок допускается только в рамках одного потока - владельца объекта.
Чтобы можно было использовать ссылки на легкие объекты в других потоках, для них требуется создать легкие ссылки для совместного доступа ("&?").
Захват легких ссылок для совместного доступа возможен только с помощью оператора группового захвата, при условии, что как миниму одна из первых ссылок будет тяжелой. т.е. с реализованным объектом синхронизации.
Пример реализации критической секции с помощью оператора группового захвата @with и статической переменной @::sync в качестве флага блокировки доступа:
# Функция для вызова в разных потоках
show( &? win1, &? win2) := {
&& @::sync := @false; # Global (static) synchronization object
@with (*sync, w1 = *win1, w2 = *win2){
... # Критическая секция
}
}
Расширенный синтаксис определения ссылок
При определении переменной, у которой допускается создание ссылок и синхронизация доступа, можно указать дополнительные параметры работы. Это делается с помощью указания системных параметров в типе разрешенных ссылок при определении объекта.
Условный пример:
&& sync; # Объект синхронизации
&&(__timeout__=1000) sync_other; # Объект синхронизации c таймаутом на блокировку
with(*sync_other) { # Блокировка с таймаутом
...
}
# Переменная value с конкретным объектом синхроинзации
&?(sync) value := 123;
with(*sync, lock = *value) { # ОК
...
}
with(*sync_other, val = *value) { # Ошибка компиляции
# для синхронизации должен использоваться sync
...
}
Автоматическое освобождение ресурсов
Менеджер контекста, кроме захвата слабых ссылок в локальные переменные и блокировки доступа, одновременно с этим позволяет выделять и освобождать ресурсы, автоматически вызывая декструктор локального объекта при выходе из блока кода оператора.
Освобождение ресурсов (вызов деструктора у объекта) происходит до попадания в ветку else, если она присутствует.
Самый популярный пример использования менеджера контекста - связанные операции открытия, чтения/записи и закрытия файла в одном блоке кода.
Например:
# Фрагмент определения класса File
File(name, mode) := :Class() {
.file ::= fopen($name, $mode); # Handle file
~File() := { # Destructor of class
@if(@this.file){
fclose(@this.file);
@this.file := _;
}
}
...
};
@with( file = :File('some_file', 'w') ) {
file.write('Oppa!');
};
Код выше открывает файл, записывает в него данные и закрывает файл при выходе из блока кода.
Этот код эквивалентен следующему на Python:
with open('some_file', 'w') as file:
file.write('Oppa!')
# Или без менеджера контекста
file = open('some_file', 'w')
try:
file.write('Oppa!')
finally:
file.close()
Более сложный пример чтение файла с одновремнным захватом ссылки на буфер и обработкой ошибок:
@with( file = :File('some_file', 'r'), buffer = *ref_buffer ) {
file.read(buffer);
} @else {
@match(@latter){
@case( :ErrorLock ) { /* Ошибка захвата ссылки ref_buffer */ };
@case( :ErrorOpenFile ) { /* Ошибка открытия для записи файла some_file */ };
@case( :ErrorReadWrite ) { /* Ошибка чтения-записи в файл some_file */ };
@default @forward; /* Остальные ошибки пробросить дальше */
};
};
Использование менеджера контекста - это гарантия закрытия файла и освобождения буфера вне зависимости от того, как будет завершён вложенный код. Распространенный паттерн использования контекстных менеджеров - блокирование и разблокирование ресурсов при многопоточном доступе к объетам, а также закрытие открытых файлов или освобождение захваченных ресурсов.
printf(format:FmtChar, ...):Int32 := %printf...;
&& holder: Integer[] = [,]; # Empty tensor
Runnable(cnt:Integer) := {
$i := 1;
@while( $i <= $cnt ) {
*holder []= $i; # Lock and add new value
$i += 1;
@if( $i % ($cnt // 10) == 0) {
@with(*holder){ # Lock only
printf(".");
}
}
}
}
$count = 10000;
thread1 = :Thread(Runnable, $count);
thread2 = :Thread(Runnable, $count);
thread3 = :Thread(Runnable, $count);
thread4 = :Thread(Runnable, $count);
thread5 = :Thread(Runnable, $count);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
@assert(*holder.size() == 5*$count);
printf("Complete %d!\n", *holder.size());
Вывод:
>..................................................
>Complete 50000!
>_