LINUX.ORG.RU

Снова о статической типизации

 ,


0

4

Пример задачи:

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

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

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

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

Усложненный вариант задачи.

В программе имеются мьютексы m1, m2… mN.

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

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

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

Ответ на: комментарий от wandrien

compilation error

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

bugfixer ★★★★★
()
Ответ на: комментарий от wandrien

Сущность, которая характеризует ограничения набора значений

Ну передают в твою функцию мьютекс, окей. Мы статически знаем, что это будет мьютекс. А в каком он будет состоянии, как ты узнаешь до того, как программа начнёт выполняться?

Nervous ★★★★★
()
Ответ на: комментарий от bugfixer

С того, что std::optional::reset() не принимает аргумента. У тебя там что-то странное написано

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

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

wandrien ★★
() автор топика
Последнее исправление: wandrien (всего исправлений: 1)

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

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

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

а что тут сложного?

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

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

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

и в чём проблема ? сделай чтобы «незахваченный» объект имел другой интерфейс. (тип/вид/класс...), и чтобы время жизни ограничивалось лексик-скоп.

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

MKuznetsov ★★★★★
()
Ответ на: комментарий от wandrien

еще про цепочечный лок мьютексов.

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

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

alysnix ★★★
()
Ответ на: комментарий от wandrien

Наверно этот «термин» несколько размытый, но я ничего другого не придумал. Если ты ткнёшь в некоторую строчку кода, то ты сможешь однозначно сказать какие мютексы в выбранный момент взяты. Альтернатива же: список взятых мютексов в общем случае зависит от состояния. Как, например, в моём примере выше на момент mutex_unlock(&list->mtx); ты не можешь без анализа переменных знать, залочен ли item->mtx или нет. Разумеется, в данном примере это бесполезная инфомрация, но там ведь мог быть и посложнее код, который лезет в item и что-то в нём делает.

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

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от monk

Да это так, шутка.

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

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

А если серьёзно, то со стороны без знания языка не очень понятно, что тут происходит.

Так то же, что и у тебя. Снаружи от lck никакого bar нету.

Могу на JS переписать.

function foo() {
  bar(); // err
  {
    lck = lock();
    lck.bar(); // ok
  }
  bar(); // err
  lck.bar; // err
}
monk ★★★★★
()
Ответ на: комментарий от maxcom

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

unlocked -> m(N1) -> m(N2) ...

Где N1 < N2 < ....

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

unlocked -> m(2) // далее в том же потоке:
unlocked -> m(1) // контракт использования нарушен

Возможно, вопрос можно решить при помощи большого количества магии на шаблонах, но как конкретно – пока не знаю.

wandrien ★★
() автор топика
Ответ на: комментарий от wandrien

М-да… значит придётся остаться с примером на Racket.

Он компилируется и при компиляции проверяет область видимости. Если нет глобальной функции bar или lck, то без определения в текущем блоке будет ругаться.

monk ★★★★★
()
Ответ на: комментарий от wandrien

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

Передаём в lock предыдущий параметр. После передачи «портим» его, чтобы пока блокировка не снята, повторно от него не сделать последовательность.

unlocked – глобальная константа, соответственно от неё может быть только одна активная последовательность.

monk ★★★★★
()
Ответ на: комментарий от monk

В голове вертится что-то насчет монад и хаскеля.

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

Что-то такое.

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

Но результат будет мало похож на обычный с++.

wandrien ★★
() автор топика
Последнее исправление: wandrien (всего исправлений: 1)
Ответ на: комментарий от wandrien

В Хаскеле нельзя запретить сделать вторую монаду (кроме IO). А в IO просто последовательность операций относительно реального мира, а не некоего unlocked.

Есть вот такая штука: https://www.tweag.io/blog/2023-01-26-linear-constraints-freeze/

Но у меня на ней мозги плавятся.

monk ★★★★★
()
Ответ на: комментарий от wandrien

выше показал пример

То есть ты хочешь этакие type guards, как в тупоскрипте, основанные на анализе путей выполнения и позволяющие вывести тип, который должно иметь значение, прошедшее или не прошедшее ту или иную проверку (например, сравнение с другим значением). Скажем, сравнили x с null — в одной ветке ифа его тип гарантированно будет null (будет включать одно возможное значение), в другой ветке — тип, из которого удалён null как возможное значение.

И ты думаешь, что у тебя получится таким образом выводить состояние мьютекса. Ну-ну. Что за недоязычок-то хоть?

Nervous ★★★★★
()
Последнее исправление: Nervous (всего исправлений: 4)

я давным давно написал такую штуку https://github.com/mdraven/mutex_order/blob/master/main.cpp

Там порядок мьютексов задаётся с помощью MutexOrder, и можно двигаться по порядку с помощью метода lock(…). Например тут:

    template<class Handler>
    int get_then(int key, mutex_order_slice<MutexOrder, MapMutex> mo_slice, Handler &&handler) {
        return mo_slice.lock(_mut, [&](mutex_order_slice<MutexOrder, BMutex> mo_slice) {
            int value = get_from_map(key, mo_slice);
            value += handler(mo_slice);
            return value;
        });
    }

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

anonymous
()

Для подобных вещей можно попробовать использовать примитив синхронизации под названием «Монитор Хоара», но я не помню точно, расставляет ли он что-то на этапе компиляции или нет.

Obezyan
()

Гм, интерес теоретический или практический?

Я думаю совершенно точно можно описать на Idris, Coq, Agda и т.п. (гм.. а что тут в т.п. положить я даже не в курсе, ребят..) Вполне вероятно получится на Хаскеле и/или Окэмле как уже тут упоминалось.

Может даже на с++ можно описать, но лично я вряд ли смогу навскидку.. я правда задачу не понял.. но это честно говоря уже детали реализации :)

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 3)

а потом мутексов станет ДВА..что вполне реальная ситуация

это каким забором придётся городить чтобы «если незалочены оба, то операция даст ошибку в компайл-тайме» ? :-)

MKuznetsov ★★★★★
()
Ответ на: комментарий от monk

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

Как-то так я это вижу.

wandrien ★★
() автор топика
Ответ на: комментарий от AndreyKl

Гм, интерес теоретический или практический?

По отношению к первой описанной задаче – практический, но её и я так знаю, как решать.

По отношению ко второй – академический.

wandrien ★★
() автор топика
Ответ на: комментарий от monk

То есть на шаблонах смысл какой:

Вот так у нас нет контроля над тем, что представляют собой foo и bar:

foo();
bar();

А нам нужно установить связь между операциями в последовательности операций. И эту связь проверять при помощи системы типов.

Вот так связь есть:

foo().bar();

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

Если не может быть блокировки, которая проживёт дольше, чем такая цепочка, то мы (наверное) сможем описать это как соотношения типов.

wandrien ★★
() автор топика

https://en.wikipedia.org/wiki/Typestate_analysis

Если прищуриться, эта сова натягивается на

https://en.wikipedia.org/wiki/Substructural_type_system#Affine_type_systems

Что в свою очередь из мейнстримных языков наиболее близко к теории реализовано в… угадай, где?


TL;DR: Молодец, ты изобрёл Rust.

intelfx ★★★★★
()
Ответ на: комментарий от intelfx

Как же вы, фанатики Раста, задобали своей клонадой.

Ты в принципе умеешь адеватно вести разговор без клоунады, или эта способность пропадает на каком-то этапе?

wandrien ★★
() автор топика
Ответ на: комментарий от anonymous

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

Для однотипных элементов можно использовать захват в порядке их адресов в памяти.

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

wandrien ★★
() автор топика
Последнее исправление: wandrien (всего исправлений: 1)

В Rust это делается паттерном создания структуры состояния на каждое состояние (Typestate Pattern), есть compile time PhantomData для удобства маркировки типа, если я верно задачу понял. Думаю проблемы написать такое же под C++ нет.

ac130kz ★★
()