Управление памятью, ссылки и совместный доступ
https://habr.com/companies/otus/articles/763810/ https://habr.com/articles/764420/
- Последовательная согласованность отсутствует (отсутствуют атомарныти типы)
Любой язык программирования так или иначе вынужден управлять оперативной памятью. В NewLang реализовано автоматическое управление памятью без сборщика мусора.
За основу была взята модель “владения” из языка Rust, но она переработана под концепцию сильных и слабых указателей (аналоги shared_ptr и weak_ptr из С++), где каждое значение в памяти может иметь только одну переменную-владельца с сильным указателем.
И когда такая переменная-владелец уходит из области видимости, счетчик ссылок уменьшается и при достжении нуля память освобождается.
Фактически, это автоматическое управление памятью с помощью подсчёта ссылок на этапе компиляции и без использования сборщика мусора.
Существование ссылок на объекты предполагает и возможность одновременного доступа к данным из нескольких потоков выполнения. Из-за чего управление памятью включает в себя и элементы межпотокового взаимодействия, так как совместное владение ссылками по любому будет требовать каких либо механизмов синхронизации доступа к разеделяемой памяти объектов.
Поэтому, при определении объекта указывается, какие типы ссылок допускается создавать на него, а так-же какая используется модель совместного доступа к переменной.
Весь механизм подсчета ссылок и проверки их корректности реализован на уровне синтаксиса. В рантайме выполняется только контроль идентификатора потока для однопоточных ссылок, а все остальные проверки выполняются во время компиляции.
Управление памятью и терминология
-
Врчную выделить или освобондить память нельзя
-
Любой объект - это ссылка на область памяти с данным. Память выделяется и освобожадется автоматически при создании/удалении объектов
-
Ссылки на объекты, с точки зрения владения, могут быть:
- Сильные/Владеющие ссылки (аналог shared_ptr из С++), а фактические, это переменная которая хранит значение объекта.
- Слабые/Не владеющие ссылки (аналог weak_ptr из С++) - указатели на другим объекты которые перед использованием требуют обязательного захвата (т.е. преобразования в сильную ссылку).
-
Ссылки на объекты, с точки зрения совместного доступа, могут быть:
- Легкие - ссылки без объекта синхронизации
- Тяжелые - ссылки с объектом синхронизации совметсного доступа (мьютексом).
-
Переменные - владельцы объектов (в них хранятся ссылки) могут быть двух видов:
- локальные (контролируемые) - область жизни локальных переменных строго ограничена и определяется правилами синтаксиса языка (аргументы и локальные переменные функций, потоков и т.д.).
- не контролируемые - глобальные или статические переменные, динамически создаваемые объекты, время жизни которых компилятор не контролирует.
-
Когда локальная переменная удаляется - уменьшается счетчик ссылок, а при достижении нуля - объект освобождается.
-
Каждый объект может иметь только одну не контролируемую переменную с сильной ссылкой и произвольное количество любых дргуих типов ссылок в локальных (контролируемых) переменных.
-
Для не контролируемых переменных разрешается делать только слабые ссылки, которые перед использованием требуется захватить, например в локальную (контролируемую) переменную.
-
Управление временем жизни объекта включает в себя не только управлением памятью, но и при необходимости, создаются механизмы синхронизации доступа к ней. Для этого при определении переменной, описываются возможные типы ссылок, которые разрешено на неё получать:
- без создания ссылок, т.е. компилятор не даст создать ссылку на данную переменную, а совместный доступ к такой переменой будет не возможен
- возможно создание легкой ссылки ("&").
Компилятору при генерации машинного кода не нужно создавать объект синхронизации доступа к переменной.
Ссылки для полей структур или объектов могут быть только легкими. - разрешено создавать ссылки с монопольным доступом ("&&"). Компилятор автоматически создает не рекурсивный мьютекс для синхронизации доступа к переменной, т.е. ссылка у этой переменной будет тяжелой.
- разрешено создавать ссылки с рекурсивным доступом ("&*"). Компилятор автоматически создает рекурсивный мьютекс (его можно захватывать несколько раз), а ссылка у этой переменной будет тяжелой.
- легкая ссылка может быть создана для совместного доступа ("&?"), но её захват и синхронизация доступа к ней возможен только при групповом захвате ссылок.
-
Все виды ссылок могут быть константными ("&^", “&&^” или “&*^”), т.е. только для чтения (и в случае константных объектов, таким ссылкам мьютекс не потребуется).
-
Захват слабой ссылки может быть индивидуальным или групповым с сохранением результата в локальную (контролируемую) переменную. Такое использование логики захвата объекта на уровне синтаксиса языка гарантирует последующее автоматическое освобождение временной переменной, что равнозначно невозможности создания циклических ссылок.
-
Переменная со слабой/не владеющей ссылкой создается только тогда, когда в правой части операции присвоения присутствует любой из операторов получения ссылки (&, &&, &* или &^, &&^, &*^).
-
Во всех остальных случаях создается переменная владелец с сильной/владеющей ссылкой (если это разрешено).
Захват ссылки и значение переменной
- Захват ссылки - это преобразование слабой ссылки в сильную с её сохранением в контролируемую переменую с инкрементом счетчика ссылок и возможностью доступа к значению объекта. Это очень похоже на заимствование (Borrowing) в Rust, так как тоже позволяет использовать данные, находящиеся во владении другой переменной, но без перехода владения.
Для захвата ссылок используются операторы:
- ‘*’ или ‘*( … )’ - автоматический выбор типа доступа (чтения/запись или только чтение)
- ‘*^’ или ‘*^( … )’ - захват доступа только для чтения
- ‘**( … )’ - групповой захват ссылок в локальные (контролируемые) переменные
- () после имени переменной - создание копии значения переменной (глубокое клонирование) ???????????????????????????????????
Упрощенный условный пример:
ref := & owner; # переменная ref - слабая ссылка на owner
ref_ro := &^ owner; # слабая ссылка на owner только для чтения
val := * ref; # Автоматический захват только для чтения
*ref = val; # Автоматический захват для чтения/записи
val := *^ ref; # Захват только для чтения
val := *^ ref_ro;
val := * ref_ro; # Автоматический захват только для чтения
*ref_ro = val; # Ошибка - ссылка только для чтения !!!
*^ ref_ro = val; # Ошибка - недопустима конструкция (захват lval - только для чтения)
- Слабую ссылку можно захватить (превратить в сильную) сохранив результат в локальной переменной, после чего работать с локальной переменной “по значению” без необходимости захватытвать ссылку при каждом обращении к переменной.
- Значения переменных со слабыми ссылками можно копировать в другие переменные без ограничений.
- Значение переменной с сильной ссылкой нельзя скопировать в другую переменную или поле объекта, но можно клонировать данные или обменяться значениями “:=:” с другой переменной с сильной ссылкой (swap).
Примеры владения:
owner := "string";
other := "string 2";
owner = other; # Ошибка - нельзя копировать!
owner = other(); # Глубокое клонирование данных
owner :=: other; # Обмен значениями (swap)
other = _; # Очистка данных объекта
- Переменную, содержащую ссылку на ссылку создать нельзя, но можно создать ссылочный тип и после этого создать переменную-ссылку на этот тип данных.
Упрощенный условный пример:
value := 123;
:RefInt := & Integer;
ref_int :RefInt := & value;
ref_ref := & ref_int;
- Если переменная владелец разрешает создавать ссылки на объект, тогда при обращении к такой переменой требуется выполнять захват объекта для обеспечения работы механизма совместного доступа.
Ссылки и совместный доступ
Управление временем жизни переменной включает в себя не только управление памятью, но и механизм синхронизации для монопольного/раздельного доступа к объектам из разных потоков.
Примеры ссылок:
& local := 123; # Разрешено создание ссылок только в текущем потоке
&& thread := 456; # Разрешено создание ссылок с монопольным доступом в любом потоке
ref := & local; # Создание слабой ссылки на local
ref2 := && local; # Ошибка! многопоточные ссылки не разрешены
ref_th := && thread; # Создание слабой ссылки на thread
# c монопольной блокировкой доступа
local += 1; # ОК
thread += 1; # Ошибка, требуется захват объекта с разеляемым доступом
*local += 1; # ОК, оператор захвата игнорируется
*thread += 1; # Захват объекта (как захват слабой ссылки)
ref += 1; # Ошибка, требуется захват слабой ссылки
ref_th += 1; # Ошибка, требуется захват слабой ссылки
*ref += 1;
*ref_th += 1;
Операторы захвата ссылки и синхронизации доступа к объекту выполняются только для одного действия над переменной. Но захват объекта синхронизации, это относительно медленная операция и выполнять её для каждого действия над переменной не рационально.
Для того, чтобы однократно захватить объект(ы) синхронизации для выполнения сразу нескольких действий над переменными можно захватить объект в локальную переменую или использовать менеджер контекста.
Пример программы
rand():Int32 ::= %rand...; # Создание объекта
@( rand():Int32 ); # Предварительное объявление (объект должен быть создан в другом месте)
rand():Int32 = ...;
usleep(usec:DWord64):None := %usleep...;
printf(format:FmtChar, ...):Int32 := %printf...;
func(count:Integer, target:String) := {
$iter := @iter( 1..$count ); # Итератор для диапазона от 1 до $count
@while( @curr($iter) ) { # Цикл, пока итератор валидный
$step := @next($iter); # Получить текущий и перейти на следующий элемент итератора
printf('Number %d from %s!', $step, $target);
usleep( rand() % 1000 ); # Случайная задержка
}
}
thread = :Thread(func, 5, 'thread');
thread.start();
func(5, 'main');
thread.join();
Number 1 from the thread!
Number 1 from the main!
Number 2 from the thread!
Number 2 from the main!
Number 3 from the thread!
Number 4 from the thread!
Number 3 from the main!
Number 4 from the main!
Number 5 from the main!
Number 5 from the thread!
Примеры ссылок:
& $local := 123; # Разрешено получение легких ссылок
&& $thread := 456; # Доступ к переменной требует захвата тяжелой ссылки
$ref := & $local; # получение слабой ссылки на local
$ref2 := && $local; # Ошибка! Тяжелой многопоточные ссылки не разрешены
$ref_th := && $thread; # Получение слабой ссылки на thread
# c монопольной блокировкой доступа
$local += 1; # ОК (для легких ссылок блокировка объекта не требуется)
$thread += 1; # ОК (захват объекта происходит автоматически)
$ref += 1; # Ошибка, требуется захват легкой слабой ссылки
$ref_th += 1; # Ошибка, требуется захват тяжолой слабой ссылки
*$local += 1; # ОК
*$thread += 1; # ОК
*$ref += 1; # ОК (только захват ссылки, блокировка игнорируется)
*$ref_th += 1; # ОК (захват ссылки и блокироваки доступа)
Менеджер контекста
Операторы захвата ссылки и синхронизации доступа к объекту выполняются только для одного действия над переменной. Но захват объекта синхронизации, это относительно медленная операция и выполнять её для каждого действия над переменной не рационально.
Для того, чтобы однократно захватить объект(ы) синхронизации для выполнения сразу нескольких действий над переменными используется менеджер контекста.