Pull to refresh

Comments 38

PinnedPinned comments

нагромождение правил

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

Или я чего-то не понимаю в изложенных концептах?

Возможно, либо я.

Мьютекс - это только концепция взаимоисключающей блокировки, которая может быть реализована как на уровне операционной системы, так и на уровне приложения или нитей в одном потоке (для большей производительности).

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

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

Спасибо! Ну наконец-то комментарий, ради которого и писалась вся статья!

Я сам понимаю, что предложенная идея хоть и рабочая в теории, но далека от совершенства. И проблем у нее как минимум две. Создание мьютексов для каждой переменной и отсутствие конкретики со ссылками в полях структур и объектов, что очень правильно подметил @kotan-11.

Именно поэтому я и решил обратиться к коллективному разуму Хабра для аудита этой идеи. И в результате Хабр опять торт в решении подобных проблем, ведь благодаря вашему комментарию удалось разрешить два последних (надеюсь) противоречия.

В описанную выше идею необходимо ввести сущность, под условным названием "вес ссылки". И дополнительно разделив все виды ссылок на легкие и тяжелые.

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

  • Легкие ссылки, это ссылки без объекта синхронизации (без мьютекса), например такие как поля структур и объектов. Но легкими могут быть ссылки и на обычные переменные.

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

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

На первый взгляд при подобных дополнениях удается разрешить упомянутые выше противоречия.

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

Мне тяжело понять && при написании, а при прочтении чужого - я, наверное, сойду с ума.

Правила описаны словами, а закорючки приведены только в качестве примера реализации. Если не нравится, можно заменить на символьную нотацию, поэтому закорючки тут совершенно не причем.

Почему нет ни слова про Хоаровское взаимодействие процессов, исключающее необходимость применения мьютексов и семафоров? Реализовано ещё в Ада, есть вариант в Go.. очень хотелось прочитать..

Если речь идет о том, что "Задачи в языке Ада основываются на хоаровском предположении о том, что параллельные процессы должны связываться через команды ввода-вывода.", то такой тип взаимодействия не покрывает все требуемые сценарии использования объектов синхронизации.

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

Вы читали самого Хоара или Википедию? Не требуется там ничего лишнего. Насколько помню, было доказано математически.

Читал Википедию и описание к Аде.
К сожалению "доказано математически" только при условии определенных ограничений, а не во всех возможных случаях, которые могут встречаться на практике.

Посмотрел вики (англ). Там оказывается есть не только его ранняя статья, которую читал, но и более поздние. Не нашел там "ограничений", кроме атомарности сообщения, но она и для изменения мьютекса требуется точно также, а механизм рандеву, кмк, существенно удобнее для взаимодествующих процессов чем блокираторы общей памяти. Кмк, "мьютекс и разделяемая память" это примерно также как программирование с delay в embeded против логики "конечных автоматов" (механизм рандеву).

Но, смотрел бегло. Если есть иные ограничения - буду признателен, покажите.

кроме атомарности сообщения

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

Что же касается конкретной реализации механизма синхронизации, то она зависит от задачи и может быть совершенно любой. Хоть блокиратор общей памяти, хоть аппаратная атомарность выполнения последовательности из нескольких инструкций, но в любой из них будет использована концепция мьютекса (т.е. взаимоисключающей блокировки).

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

нагромождение правил

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

Или я чего-то не понимаю в изложенных концептах?

Возможно, либо я.

Мьютекс - это только концепция взаимоисключающей блокировки, которая может быть реализована как на уровне операционной системы, так и на уровне приложения или нитей в одном потоке (для большей производительности).

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

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

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

Подобное утверждение было бы справедливо при применении его к ограничениям растового борроу чекера (хотя и тот иногда выёживается тогда, когда не нужно. Часть этих случаев призван искоренить Polonius), однако все эти запреты на утечки и всякие &^* надёжности не добавляют.

автоматическиможет выбрать оптимальный вариант реализации объекта синхронизации

Всё-таки я недопонял суть изложенного. В таком случае, да, если компилятор будет чётко определять ситуации, в которых примитивы синхронизации не нужны, оверхеда не будет. Однако, это лишь укрепляет мою веру о том, что вы лишаете программиста всякой низкоуровневой свободы. Оверхед должен быть явным. Сама по себе неявность в данном случае противоречит основам функционального программирования и уводит нас чуть ли не в какие-то, простите за выражение, ООП-шные дебри.

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

# Обычная переменная без раздельного доступа
# и без возможности создания ссылки на объект
let val := 123;
let val_err := &val; # Ошибка !!!!

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

то такой объект нельзя передать параметром в функцию

Нельзя передать ссылку на объект, но сам объект можно передать по значению (копию объекта).

Параметр this в С++ действительно является ссылкой на объект, но и С++ не управляет ссылками, как это предлагается делать на уровне синтаксиса языка, тогда как в случае реализации описанной выше концепции, аналогом ссылки this из C++ будет:

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

То есть реализация this из С++ хоть и является ссылкой, но предназначена немного для других целей.

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

Когда не только локальные, но и глобальные переменные.

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

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

И где расположен объект синхронизации доступа. Либо у каждого поля с многопоточной ссылкой, либо у всего объекта целиком. Сейчас я склоняюсь к блокировке всего объекта целиком, так как это мне кажется проще при реализации. Но не исключено, что данная концепция потребует каких-либо доработок.

Пока это выглядит как "пусть мыши станут ёжиками". Идея нуждается в доработке и не может быть оценена.

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

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

Теперь о том, как это всё работает в многопотоке:

Неизменяемые объекты свободно шарятся между всеми потоками. Для доступа к ним мне нужна синхронизация, н нужны mitexes и atomics.

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

Один поток может иметь слабые ссылки на объекты другого потока. Но обращаться к ним синхронно он не может. Зато он может посылать этим объектом асинхронные сообщения, которые будут исполнены в контексте потока объекта - получателя. Этот механизм очень похож на рандеву.

Если асинхронное сообщение несёт на себе в качестве параметра какой-то изменяемымй объект, этот объект логически передаётся из потоков поток никакого копирования при этом конечно же не происходит.

Эта схема гарантирует

  • отсутствие дедлоков, поскольку работающий код вообще никогда не хватает мьютексов,

  • отсутствие лайвлоков, поскольку атомарные операции тоже не используются,

  • отсутствие data races, поскольку каждая операция выполняется в своём потоке как изолированная транзакция.

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

Эта схема проста с точки зрения реализации, эффективна и безопасна.

Большое спасибо за развернутое описание работы Аргентум в многопоточной среде! Я на что-то подобное и рассчитывал, когда упоминал этот язык у себя в статье. Тем более, что часть идей я так или иначе подсмотрел именно там :-)

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

Вот это не очень понятно:

Объект может иметь только одну не контролируемую переменную с сильной ссылкой

Если нужно, чтобы объект MainWindow владел своими компонентами: Menu, Toolbar и StatusBar - то ему такое нельзя?

Владеть можно только одному объекту, например MainWindow будет владеть компонентами: Menu, Toolbar и StatusBar. Если потребуется передать ссылку на эти компоненты, например в функцию, то можно будет передавать только слабую ссылку.

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

Ограничений на переменные в составе объекта нет.
Но может существовать не более 1 сильной ссылки на объект в не контролируемой переменной. Тогда как сильные ссылки на объект в контролируемых переменных могут быть в любых количествах.

Как по мне, очень много оверхеда.

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

При этом, мы не зашищены от дедлоков (если один поток вычисляет a+b, а другой b+a, при этом a,b - слабые ссылки).

Непонятно, как синтаксически указать тип каждого разыменовывания в цепочках перехода по ссылкам наподобие

if (root->left->value + root->right->value == object->value->sum)

Спасибо! Ну наконец-то комментарий, ради которого и писалась вся статья!

Я сам понимаю, что предложенная идея хоть и рабочая в теории, но далека от совершенства. И проблем у нее как минимум две. Создание мьютексов для каждой переменной и отсутствие конкретики со ссылками в полях структур и объектов, что очень правильно подметил @kotan-11.

Именно поэтому я и решил обратиться к коллективному разуму Хабра для аудита этой идеи. И в результате Хабр опять торт в решении подобных проблем, ведь благодаря вашему комментарию удалось разрешить два последних (надеюсь) противоречия.

В описанную выше идею необходимо ввести сущность, под условным названием "вес ссылки". И дополнительно разделив все виды ссылок на легкие и тяжелые.

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

  • Легкие ссылки, это ссылки без объекта синхронизации (без мьютекса), например такие как поля структур и объектов. Но легкими могут быть ссылки и на обычные переменные.

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

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

На первый взгляд при подобных дополнениях удается разрешить упомянутые выше противоречия.

Тут вводится новая сущность "группа ссылок".
Непонятно, то ли группа находится в одном объекте и описывается в момент определения класса.
То ли эта группа для операции захвата.
Вроде первое, потому что тип ссылки менять нельзя.

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

Прошу прощения. Действительно, в статье на Хабре я не стал описывать операторы захвата ссылок, так как и без этого материал был довольно сложным для восприятия. Но когда отвечал вам на предыдущий комментарий, сам держал в голове эти сущность и оперировал ими как известными понятиями.

На самом деле тут все просто.
Захватить ссылку и превратить слабую ссылку в сильную можно двумя операторами.

  • Оператором захвата одиночной ссылки - это звездочка перед именем переменной со ссылкой, т.е. обычный value = *ref;.

  • Или оператором захвата группы ссылок - по сути менеджером контекста, который на входе берет произвольное количество слабых ссылок и превращает их в сильные в локальных переменных в теле оператора. Но управление в тело оператора будет передано только в случае захвата всей группы ссылок сразу, например вот так:

    with( value1 = *ref1, value2 = *ref2, value3 = *ref3){
        value3 = value1+value2;
    } else {
        # Обработка ошибки захвата любой из группы ссылок
    }

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

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

То есть, если в первом модуле написано

with( value1 = *ref1, value2 = *ref2, value3 = *ref3)

а в другом написано

with( value1 = *ref2, value2 = *ref1, value3 = *ref3)

то всё, это не будет компилироваться?

А если раздельная трансляция?

Да вообще, откуда компилятор знает, что лежит внутри ref1 и ref2, это могут быть параметры, которые приходят снаружи.

А если раздельная трансляция?

Тут должна быть трансляция как в clang (обрабатывается модуль целиком), а не линковка отдельных объектные файлов как в С/С++

то всё, это не будет компилироваться?

Да, тут как раз и начинает работать ваше замечание в исходном комментарии про оверхед и дидлок.

Компилятор знает про все ссылки и их типы, значит он будет знать и какой тип ссылки в ref1, ref2 и ref3. Даже если ссылки пришли снаружи, то их тип все равно должен быть указан заранее, как и способ их блокировки (рекурсивная или не рекурсивная).

А за счет того, что модуль обрабатывается целиком, то один из этапов анализа AST может быть поиск конфликтов в параметрах with. Ведь вся эта информация будет доступа во время компиляции исходников.

В теории всё гладко, сложности вылезут в реализации

def show(&win1, &win2) {
    with (w1 = *win1, w2 = *win2) ...
}

Пока всё нормально

show(toolbar, statusbar);

тут тоже всё нормально

var bottom, top;
if (flag) {
    bottom = toolbar;
    top = statusbar;
} else {
    bottom = statusbar;
    top = toolbar;
}
show(bottom, top);

Ну может не совсем попал в синтаксис, но идея надеюсь понятна - в зависимости от флага менять местами toolbar и statusbar, при этом передавая их в функцию рендера show, которая их лочит последовательно. Я думаю, тут компилятор скажет "кряк", как та японская пила из анекдота.

О, сложности безусловно вылезут, и не один раз!

Чтобы уточнить ваш пример, нужно обязательно добавить, что объекты toolbar и statusbar созданы с разрешением создания для них многопоточных ссылок, а функция show (или аналогичная с оператором with для двух аргументов) может вызываться в разных потоках.

Тогда в теории действительно можно словить дидлок без шанса анализа ситуации во время компиляции, так как порядок последовательности блокировки меняется в рантайме.

Но в этом случае я бы попробовал сделать (точнее, буду делать) вывод предупреждения, что в операторе with захватываются и блокируются две тяжелые ссылки, поэтому возможен дидлок.

А сам код предложил бы переписать немного по другому.

Сделать объекты toolbar и statusbar с легкими ссылками (без мьютеска), а для синхронизации использовал переменную flag (ну или отдельную переменную в самой функции show, т.е. своего рода критическая секция).

def show(&win1, &win2) {
    static bool && sync;
    with (*sync, w1 = *win1, w2 = *win2) ...
}

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

И только первое оправдывает создание нового языка (что и сделал rust), а для второго можно классов-обёрток-указателей и прочей мишуры напридумывать в обычном C++

Самому думать придется всегда. А свой язык..., ведь повышение безопасности при работе с памятью, это же может быть не единственная фича языка.

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

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

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

Очень смелое заявление - создавать язык простой для изучения, при этом поддерживающий ВСЁ подмножество C++

Очень смелое заявление - создавать язык простой для изучения, при этом поддерживающий ВСЁ подмножество C++

Самое интересное, что это можно сделать очень легко, просто разрешив делать вставки исходного кода на C++ и обеспечив их взаимодействие с основным кодом программы. Хотя в этом случае нельзя будет сказать, что поддерживаются именно ВСЕ возможности, но точно подавляющее большинство.

Это значит, что вряд ли получится написать свой компилятор. Придётся транспилировать в C++ и скармливать сишному компилятору. Соответственно, отладка будет идти по C++ коду и прочие неудобства.

Компилятору придется скармливать по любому, если в коде будут вставки на C++, но во всех остальных случаях транспилировать не обязательно. Либо это дело скрывать, используя clang для динамической компиляции кода. В любом случае, до этого пока еще очень далеко.

Sign up to leave a comment.

Articles