Управление памятью и совместный доступ

В NewLang реализовано безопасное автоматическое управление памятью без сборщика мусора.

Автоматическое освобждение памяти реализовано за счет использования двух типов указателей - сильные и слабые ссылки (shared_ptr и weak_ptr в терминологии С++) и автоматического применения принципа RAII. Причем создание сильных циклических ссылок запрещено на уровне типов данных (определений классов) любой вложенности и проверяеться статическим анализатором во время компиляции исходного кода программы.

Проблема гонок данных при обращении к памяти из разных потоков решается за счет автоматической межпотоковой синхронизации. Тип синхронизации доступа к переменной требуется указывать при её определении, после чего захват и освобождение объекта синхронизации будут происходить автоматически при разименовании ссылки.

Переменные (объекты) и терминология

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

По времени жизни объекты в NewLang может разделить на два класса:

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

Виды переменных могут быть:

  • Переменная по значению (variable by value) - данные хранятся непосредственно в самой переменной. Это переменные в их классическом понимании, когда копия переменной создает дубликат исходного значения, а изменение копии переменной никак не влияет на исходную. Создать ссылку на переменную по значению нельзя (для этих целей нужно использовать ссылочную или защищенную переменную).

  • Ссылочная переменная (reference variable) - в переменой находится только сильный (владеющий) указатель на переменную по значению, тогда как пямять под неё выделяется статически или в общей куче. Копия ссылочной переменной создает копию указателя и увеличивает счетчик владений. Ссылочная переменная предназначена для использования только в текущем потоке и не имеет накладных расходов на использование механизма межпотоковой синхронизации доступа.

  • Ссылочная переменная может быть защищенной (guard variable) - со встроенным механизмом межпотоковой синхронизации. Создание копии защищенной переменной копирует только сильный указатель и увеличивает счетчик владений. Предназначена для данных к которым можно получить доступ из разных потоков.

  • Переменная ссылка (link variable) - в переменой находится только слабый (не владеющий) указатель на ссылочную переменную. Создание копии переменной-ссылки копирует только слабый указатель без увеличения счетчика владений. Для доступа к данным с помощью переменной-ссылки используется оператор захвата (который выполняет блокировку доступа и преобразование слабого указателя в сильный).

Захват (разыменование/блокировка) ссылок и ссылочных переменных

Доступ к данным у переменных по значению возможен в любой момент, тогда как для досутпа к данным у переменных-ссылок и ссылочных переменных требует предварительного захвата.

Захват (разыменование/блокировка) ссылок и ссылочных (защищенных) переменных производится всегда в автоматическую переменную, которая является временным владельцем сильной ссылки и выполняет функции удержания объекта межпотоковой синхронизации в стиле std::lock_guard, время жизни которого ограничено текущей областью видимости и управляется компилятором автоматически.

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

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

flowchart TB

    shared(["`
            Reference or guard variable (*shared_ptr*)
        `"])--->|"`Copy reference or guard variable`"|local[shared_ptr
            increases owner counter
        ]
    
    shared-.->|"`Get link from shared variable`"|ref1[weak_ptr]
    shared==>|"`**Capture** guard to auto variable, increases *shared_ptr* counter and **lock** synchronization object (if present)`"|take1[weak_ptr]
    shared-.->|"`Get link from shared variable`"|ref[weak_ptr]
    local==>|"`**Capture** guard to auto variable, increases *shared_ptr* counter and **lock** synchronization object (if present)`"|take1[weak_ptr]


    subgraph reference[" "]
        ref1-.-|Link copying without restrictions|ref3[weak_ptr]
        ref[weak_ptr]
    end

    ref1==>|"`Get a shared pointer with an incremented ownership counter, dereference it into an automatic variable, and **capture** the synchronization object (if present)`"|take1((("`
        The dereferenced pointer and the captured synchronization object (if present) in a local/automatic variable
        `")))

    click take1 "#lock"

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

  • перед имененм символ захвата/разименования ("*") - создается ссылочная переменная без контроля совместного досутпа для использования только в текущем потоке, в том числе для асинхронного программирования.

Декларация вида переменной, в том числе модель совместного доступа для защищенных перемнных, указывется в виде типа (номера ссылки) ссылки перед именем переменной:

  • ничего не указано - создается переменная по значению, а компилятор не даст создать ссылку на переменную и совместный доступ к ней будет не возможен.
  • символ захвата/разименования ("*") - создается ссылочная переменная без контроля совместного досутпа для использования только в текущем потоке, в том числе для асинхронного программирования.
  • символ простой ссылки ("&") - создается ссылочная переменная без объекта синхронизации доступа для использования только в текущем потоке (при захвате ссылки проверяется идентификатор потока).
  • символ защищённой ссылки с монопольным доступом ("&&") - создается защищённая переменная с объектом межпотоковой синхронизации в виде обычного мьютекса.
  • символ защищённой ссылки с рекурсивным доступом ("&*") - создается защищённая переменная с объектом сихнхронизации в виде рекурсивного мьютекса (его можно захватывать в одном потоке несколько раз).
  • символ простой ссылки для совместного доступа ("&?") - создается ссылочная переменная без объекта синхронизации доступа, для работы с которой требуется использовать групповой захват ссылок.

Все виды ссылок могут быть константными (иммутабельными) ("&^", “&?^”, “&&^” или “&*^”), т.е. только для чтения (и в случае константных объектов, таким ссылкам объект синхронихации не потребуется даже для защищенных ссылок).

Переменная со слабой ссылкой создается тогда, когда в правой части оператора создания/присвоения перед именем общей переменной присутствует любой из операторов получения ссылки (&, &&, &* или &^, &&^, &*^).

При операциях копирования у ссылочных переменных копируется только указатель с увеличением счетчика владения. Если следует копировать не указатель, а сами данные, тогда в правой части оператора присвоения перед именем ссылочной переменной следуюет указть оператор захвата ("*").

Пример создания локальных и ссылочных переменных и их копирование:

    value1 := 1; # Create variable by value
    copy1 := value1; # Create new variable by value from value1

    *owner := 2; # Create reference variable
    local1 := owner; # Error create copy reference to local variable
    local2 := *owner; # Ok. Allow clone value to local variable
    *copy_owner := owner; # Error create from reference variable
    *clone_owner := *owner; # Clone value to new reference variable
    {
        local3 := owner; # Error!!! Copy reference to local variable
        local4 := *owner; # Clone value to local variable
        *copy2_owner := owner; # Copy reference to new reference variable
        *clone2_owner := *owner; # Clone data to new reference variable

        copy2_owner = 23; # Reference variable capture automatically (exclusive access) ????????????????
        *copy2_owner = 23; # The owner variable has new value 23
        # clone2_owner has old value 2
    }

Пример создания защищенных переменных и ссылок:

    & common: = 3; # Guard variable
    && multi : = 3; # Guard variable + mutex

    common_link := & common; # Weak pointer to common variable
    *common_link = 6; # Set new value 6 to common

    multi_link := && multi ; # Weak pointer to multi variable
    *multi_link = 10; # The multi has new value 10


    & copy_common := common; ?????
    * copy_common := common; ?????
    & copy_common := & common; ?????
    * copy_common := & common; ?????
    & copy_common := *common; # Copy data from common
    * copy_common := *common; # Copy data from common
    {

    }


    && multi_copy := multi; ?????

Оператор создания легкой однопоточной ссылки контролирует идентификатор потока, в котором ссылка создается и эта проверка гарантирует защиту данных от ошибок конкуретнного доступа.

Захват ссылки/переменной

Любые вычисления и преобразования данных возможны только с захваченными переменными.

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

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

Для захвата переменных используются следующие операторы:

  • *’ - автоматический выбор типа доступа (чтения/запись или только чтение)
  • *^’ - захват доступа только для чтения
  • **( … )’ - групповой захват ссылок в локальные (автоматические) переменные

Упрощенный условный пример:

    ref := & value;  # переменная ref - слабая ссылка на value
    ref_ro := &^ value;  # слабая ссылка на value только для чтения

    val := *ref;  # Автоматический захват ref только для чтения 
    *ref = val;    # Автоматический захват ref для чтения/записи
    obj.*ref = 123; # Запись значения по ссылке ref - члена класса/структуры
    *ptr.*ref = 123; # Запись значения по ссылке ref - члена класса/структуры, которая тоже является ссылкой

    val := *^ ref; # Захват ref только для чтения

    val := *^ ref_ro; 
    val := * ref_ro;  # Автоматический захват только для чтения
    *ref_ro = val;    # Ошибка - ссылка только для чтения !!!
    *^ ref_ro = val;  # Ошибка - (захват lval - только для чтения)

Ссылки и совместный доступ

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

Примеры ссылок:

    & local := 123;     # Разрешено создание ссылок только в текущем потоке
    && thread := 456;   # Разрешено создание ссылок с монопольным доступом в любом потоке
    
    ref := & local;     # Создание слабой ссылки на local
    ref2 := && local;   # Ошибка! многопоточные ссылки не разрешены
    ref_th := && thread;  # Создание слабой ссылки на thread 
                          # c монопольной блокировкой доступа

    local += 1;     # Ошибка, требуется захват ссылочной переменной
    *local += 1;    # ОК

    thread  += 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!

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

Ссылки не являются рекурсивными и создать переменную, содержащую ссылку на ссылку нельзя, так как оператор получения ссылки всегда возвращает слабую ссылку на свободную переменную.

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

Упрощенный условный пример:

    & value: Integer := 123; # Value
    & ref_int :^Integer := & value; # Link for value
    ref_ref := & ref_int; # Link for link type
    & value := 123; # Value for link
    :RefInt := & Integer; # Reference type
    & ref_int :RefInt := & value; # Link for value
    ref_ref := & ref_int; # Link for link value