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

https://habr.com/ru/companies/otus/articles/763810/ https://habr.com/ru/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;   # ОК (захват ссылки и блокироваки доступа)

Менеджер контекста

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

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