Переосмысление концепции подсчета ссылок и полный отказ от сборщика мусора
Фундаментальной (по моему мнению) проблемой множества языков программирования является неявное использование ссылок (ссылочных переменных).
И проблема не в том, что с помощью ссылок изменяются какие-то общие данные, а в том, что часто это делается неявным образом, из-за чего программист должен всегда помнить об особенностях таких переменных.
Еще одной проблемой (или следствие предыдущей) является многопоточное и/или асинхронное выполнение программы, когда к одной и той же области данных может быть получен доступ из разных потоков приложения, что в купе с неявным доступом по ссылкам еще больше усугубляет ситуацию.
А как бы хотелось отдать эти проблемы на откуп компилятору! Чтобы компьютер сам автоматически проверял корректность доступа по ссылкам, в том числе и из разных потоков и чтобы все это делалось во время компиляции приложения без накладных расходов в рантайме!
И если такое будет возможно, тогда постепенно уйдет в прошлое целая эпоха сборщиков мусора с их неожиданными зависаниями программ в произвольные моменты времени и разными мудреными алгоритмами поиска мертвых указателей и циклических ссылок!
Неявные ссылки
Модель передачи аргументов Python не является ни «передачей по значению», ни «передачей по ссылке», а описывается скорее как «передача по ссылке на объект». И в зависимости от типа объекта, который передается в функцию, переменные-аргументы ведут себя по разному.
Поэтому принято считать, что неизменяемые объекты в Python в качестве аргументов передаются по значению, тогда как изменяемые объекты (списки (list), множества (set) и словари (dict)), всегда передаются по ссылке.
Но поскольку в Python отсутствует само понятие ссылка (нет разделения на примитивные и ссылочные типы), то все это происходит неявным образом, что с учетом отсутствия в Python строгой типизации, добавляет очень много возможностей выстрелить в ногу себе или товарищу.
А вот в С++ есть и указатели (pointer) и ссылки (reference), причем ссылки можно считать синтаксическим сахаром над указателями, которые упрощают чтение и написание кода. К сожалению, одновременно с этим они так же добавляют и путаницы, т.к. при изменении ссылочной переменной в реальности происходит обращение к объекту, на который эта ссылка указывает, но визуально в тексте программы ссылочная переменная ничем не отличаются от остальных переменных «по значению», ведь для работы с ними не требуется выполнять разименовывание указателя.
Но основные проблемы при работе со ссылочными переменными могут возникать в операциях создания переменной или при их копировании. Если переменная по значению создают новую копию данных, тогда как копия ссылочной переменной новые данные не создает. Ведь копируется только неявная ссылка на общие данные и их изменение будет отражаться сразу во всех копиях таких переменных одновременно.
Хочу обратить внимание, что проблемы тут не в поведении ссылочных переменных (оно и должно быть ровно таким), а в том, что для создания, копирования или обращения к переменных по значению и к ссылочных переменным используется один и тот же оператор, из-за чего в выражении визуально невозможно отличить переменную по значению от ссылочный переменные. И именно такая неявная работа со ссылками и является основанием для критики (при работе с явными ссылками такой проблемы нет, так как их необходимо явно разименовывать).
Конкурентный доступ
Еще одна проблема проявляется только во время многопоточного и/или асинхронного выполнения кода. Когда к одной и той же области данных может быть получен доступ из разных потоков приложения. И что в купе с неявным обращением по ссылкам еще больше усугубляет ситуацию. Причем, проблема конкурентного доступа проявляется, в том числе и при работе с обычными ссылками. И хотя разименовывать указатель для доступа к данным необходимо выполнять явным образом, проблему с конкуретным доступом по ссылке из разных потоков все равно приходится решать программисту самостоятельно.
Формализация концепции переменных
Чтобы описать концепцию использования переменных для какого-нибудь нового языка программирования, сперва нужно разделить все переменные по их времени жизни на статические и локальные (автоматические):
- Статические — это глобальные переменные, функции и типы данных к которым можно обратиться из любого другого участка кода или модуля. Статические переменные (объекты) создаются как правило в куче и сохраняют свое значение при выходе из блока кода, где были определены (т.е. после выхода из текущей области видимости).
- Автоматические или локальные переменные, это аргументы функций и переменные в выражениях, которые создаются компилятором в автоматическом режиме как правило на стеке. Локальные и автоматические переменные доступные только изнутри того лексического контекста, в котором они были определены, а их значения уничтожаются при выходе из блока кода, где они были созданы.
И определить следующие виды переменных:
- переменная по значению (variable by value) — данные хранятся непосредственно в самой переменной, а при копировании переменной создается новая копия данных.
- общая переменная (common variable) — классическая переменная по ссылке. В переменой хранится только указатель на данные (например shared_ptr) который увеличивает счетчик владений данными. При копировании переменной копируется только указатель и увеличивается счетчик владений. Для такой переменой нельзя получить переменную-ссылку.
- разделяемая переменная (shared variable) — тоже переменная по ссылке в которой хранится указатель на данные со счетчиком владений, но копировать саму переменную запрещено (можно сделать только swap — обмен значениями), но для такой переменной можно получить переменную-ссылку.
- переменная ссылка — слабый указатель на разделяемую переменную, который не увеличивает счетчик владений (weak_ptr). Перед использованием слабый указатель необходимо преобразовать в сильный или сохранить в общую переменную.
Тогда правилами для работы с такими видами переменных будут следующие:
- Переменные по значению и переменные-ссылки могут копироваться из одной переменной в другую аналогичного вида без каких либо ограничений.
- Общая переменная с сильной ссылкой может быть скопирована только в локальную переменную более низкого уровня или передана в качестве аргумента в функцию. Операция обмена значениями (swap) для общей переменной запрещена.
- Для разделяемой переменной запрещены операции копирования, но можно выполнять обмен значениями (swap) и можно получать слабую ссылку на разделяемую переменную (точнее на данные в разделяемой переменной).
Ссылки и конкурентный доступ
При определении переменной важен не только тип её данных и время жизни, но и возможность получения доступа к ней из других потоков приложения. И если такой доступ возможен, то необходимо каким нибудь обратом сообщить об этом компилятору, чтобы он мог использовать данную информацию для управления механизмом синхронизации при разделяемом доступе к объектам.
Доступ из разных потоков возможен только у разделяемых переменных. Причем совместный доступ реализуется за счет наличия переменных-ссылок, которые разрешено создавать для разделяемых переменных.
В этом случае, уже при определении такой переменной можно будет указывать тип разделяемого доступа, за счет чего появляется возможность автоматической реализации механизмов межпотоковой синхронизации на уровне синтаксиса языка во время работы компилятора, например следующим образом:
Типы ссылок с точки зрения разделяемого доступа могут быть:
- без ссылок, т.е. компилятор не даст получить ссылку на переменную и разделяемый доступ к ней будет не возможен
- простые ссылки — это обычные ссылки, но их разрешено использовать только в текущем потоке, а компилятору при генерации машинного кода не нужно создавать объект синхронизации доступа.
- управляемые ссылки с монопольным доступом — это ссылки с обычным мьютексом, который компилятор создает автоматически для управления разделяемым доступа к переменной.
- управляемые ссылки с рекурсивным доступом — это ссылки с рекурсивным мьютексом (его можно захватывать в одном потоке несколько раз).
Операторы для ссылок
Так как любая переменная-ссылка является слабой (weak_ptr), перед использовать её требуется преобразовать в сильную ссылку. А с учетом возможного конкурентого доступа из разных потоков, требуется ещё выполнять и захват объект межпотоковой синхронизации (если он используется). Для данных целей используется два оператора захвата ссылок.
Захват ссылки — это захват объекта синхронизации доступа к переменной (при его наличии) и преобразование слабой ссылки в сильную с инкрементом счетчика владения и сохранением результата в локальную (автоматическую) переменную.
Такое использование логики захвата объекта на уровне синтаксиса языка, гарантирует последующее автоматическое освобождение временной переменной, что равнозначно невозможности создания сильных циклических ссылок, которые могут приводить к утечкам памяти.
В завершении
Я специально не стал приводить никаких примеров с исходным текстом, так как способы определение переменных в разных языках программирования очень сильно отличаются, тогда как мне хотелось сконцентрировать внимание читателей не семантике какого-то одного конкретного языка, а на сути поднятой проблемы.
По этой же причине я немного упростил описание концепции и не стал вдаваться в различные тонкости реализации механизмов межпотоковой синхронизации и не упомянул про влияние константности (иммутабельности) на разделяемый доступ, так как это тоже может увести в сторону не принципиальных деталей.
Ведь основная идея, это реализация методики подсчета количества ссылок на объекты и использование сильных и слабых указателей под полным контролем компилятора.
Другими словами, это реализация техники подсчета ссылок на уровне синтаксиса языка во время компиляции и которой не требуется сборщик мусора.