Java и C # обеспечивают безопасность памяти, проверяя границы массивов и разыменования указателей.
Какие механизмы можно было бы внедрить в язык программирования, чтобы предотвратить возможность состояния гонки и тупиков?
Java и C # обеспечивают безопасность памяти, проверяя границы массивов и разыменования указателей.
Какие механизмы можно было бы внедрить в язык программирования, чтобы предотвратить возможность состояния гонки и тупиков?
Ответы:
Гонки происходят, когда у вас есть одновременный псевдоним объекта и, по крайней мере, один из псевдонимов является мутирующим.
Таким образом, чтобы предотвратить гонки, вы должны сделать одно или несколько из этих условий неверными.
Различные подходы затрагивают различные аспекты. Функциональное программирование подчеркивает неизменность, которая устраняет изменчивость. Блокировка / атомика убирают одновременность. Аффинные типы удаляют псевдонимы (Rust удаляет изменяемые псевдонимы). Актерские модели обычно удаляют псевдонимы.
Вы можете ограничить объекты, которые могут быть псевдонимами, чтобы легче было избежать вышеуказанных условий. Вот где вступают каналы и / или стили для передачи сообщений. Вы не можете использовать псевдоним произвольной памяти, просто конец канала или очереди, которая устроена так, чтобы быть свободной от гонки. Обычно избегая одновременности, то есть блокировок или атомарности.
Недостатком этих различных механизмов является то, что они ограничивают программы, которые вы можете написать. Чем более тупо ограничение, тем меньше программ. Так что никакие псевдонимы или изменчивость не работают, и их легко рассуждать, но они очень ограничены.
Вот почему Руст вызывает такой ажиотаж. Это инженерный язык (в отличие от академического), который поддерживает псевдонимы и изменчивость, но проверяет компилятор, что они не встречаются одновременно. Хотя это и не идеал, он позволяет безопасно писать больший класс программ, чем многие его предшественники.
Java и C # обеспечивают безопасность памяти, проверяя границы массивов и разыменования указателей.
Важно сначала подумать о том, как это делают C # и Java. Они делают это путем преобразования неопределенного поведения в C или C ++ в определенное поведение: сбой программы . Нулевые разыменования и исключения индекса массива никогда не должны быть обнаружены в правильной программе на C # или Java; они не должны происходить в первую очередь, потому что в программе не должно быть этой ошибки.
Но это я думаю не то, что вы подразумеваете под своим вопросом! Мы могли бы довольно легко написать «безопасную для взаимоблокировки» среду выполнения, которая периодически проверяет, есть ли n потоков, взаимно ожидающих друг друга, и завершает программу, если это произойдет, но я не думаю, что это удовлетворит вас.
Какие механизмы можно было бы внедрить в язык программирования, чтобы предотвратить возможность состояния гонки и тупиков?
Следующая проблема, с которой мы сталкиваемся в связи с вашим вопросом, заключается в том, что «гоночные условия», в отличие от тупиков, трудно обнаружить. Помните, что то, что мы ищем в безопасности потоков, это не устранение гонок . Мы стремимся сделать программу правильной, независимо от того, кто выиграет гонку ! Проблема с условиями гонки состоит не в том, что два потока работают в неопределенном порядке, и мы не знаем, кто собирается закончить первым. Проблема с условиями гонки заключается в том, что разработчики забывают, что некоторые порядки завершения потоков возможны, и не учитывают эту возможность.
Таким образом, ваш вопрос в основном сводится к тому, "есть ли способ, которым язык программирования может гарантировать, что моя программа верна?" и ответ на этот вопрос на практике - нет.
До сих пор я только критиковал ваш вопрос. Позвольте мне попытаться переключить передачи здесь и обратиться к духу вашего вопроса. Есть ли выбор, который могли бы сделать разработчики языка, чтобы смягчить ужасную ситуацию с многопоточностью?
Ситуация действительно ужасна! Правильно получить многопоточный код, особенно на слабых архитектурах моделей памяти, очень и очень сложно. Поучительно подумать, почему это сложно:
Таким образом, есть очевидный способ, которым дизайнеры языка могут сделать вещи лучше. Отказаться от производительности побеждает современные процессоры . Сделайте так, чтобы все программы, даже многопоточные, имели чрезвычайно сильную модель памяти. Это сделает многопоточные программы во много-много раз медленнее, что напрямую работает против причины, по которой в первую очередь многопоточные программы: для повышения производительности.
Даже если оставить в стороне модель памяти, существуют и другие причины, по которым многопоточность затруднена:
Этот последний пункт требует дальнейшего объяснения. Под «компонуемым» я подразумеваю следующее:
Предположим, мы хотим вычислить int с учетом double. Мы пишем правильную реализацию вычисления:
int F(double x) { correct implementation here }
Предположим, что мы хотим вычислить строку с использованием int:
string G(int y) { correct implementation here }
Теперь, если мы хотим вычислить строку, заданную двойным:
double d = whatever;
string r = G(F(d));
G и F могут быть составлены в правильное решение более сложной проблемы.
Но у замков нет этого свойства из-за тупиков. Правильный метод M1, который принимает блокировки в порядке L1, L2, и правильный метод M2, который принимает блокировки в порядке L2, L1, нельзя использовать в одной и той же программе без создания неверной программы. Блокировки делают так, что вы не можете сказать, что «каждый отдельный метод является правильным, так что все это правильно».
Итак, что мы можем сделать как дизайнеры языка?
Во-первых, не ходи туда. Несколько потоков управления в одной программе - плохая идея, а совместное использование памяти между потоками - плохая идея, поэтому не помещайте ее в язык или среду выполнения.
Это, очевидно, не стартер.
Давайте обратим наше внимание на более фундаментальный вопрос: почему у нас есть несколько потоков в первую очередь? Есть две основные причины, и они часто связаны в одно и то же, хотя они очень разные. Они объединены, потому что они оба об управлении задержкой.
Плохая идея. Вместо этого используйте однопоточную асинхронность через сопрограммы. C # делает это красиво. Ява, не очень хорошо. Но это основной способ, которым современные разработчики языков помогают решить проблему с многопоточностью. await
Оператор в C # ( под влиянием F # асинхронные рабочие процессы и предшествующий уровень техники) в настоящее время включены во все более и более языках.
Языковые дизайнеры могут помочь, создавая языковые функции, которые хорошо работают с параллелизмом. Подумайте, например, как естественным образом распространяется LINQ на PLINQ. Если вы разумный человек, и вы ограничиваете свои операции TPL ограниченными процессором операциями, которые являются высокопараллельными и не разделяют память, вы можете получить здесь большие победы.
Что еще мы можем сделать?
C # не позволяет вам ждать в замке, потому что это рецепт для взаимоблокировок. C # не позволяет вам блокировать тип значения, потому что это всегда неправильно; Вы блокируете коробку, а не значение. C # предупреждает вас, если вы используете псевдоним volatile, потому что псевдоним не навязывает семантику получения / выпуска. Существует гораздо больше способов, с помощью которых компилятор может обнаружить общие проблемы и предотвратить их.
C # и Java допустили огромную ошибку проектирования, позволив вам использовать любой эталонный объект в качестве монитора. Это поощряет всевозможные дурные практики, которые затрудняют поиск тупиковых ситуаций и затрудняют их статическое предотвращение. И это тратит байты в каждом заголовке объекта. Мониторы должны быть получены из класса монитора.
STM - прекрасная идея, и я поиграл с игрушечными реализациями в Haskell; это позволяет вам гораздо более элегантно составлять правильные решения из правильных деталей, чем решения на основе блокировок. Однако я не знаю достаточно о деталях, чтобы сказать, почему его нельзя заставить работать в масштабе; Спроси Джо Даффи в следующий раз, когда увидишь его.
Было проведено много исследований языков на основе исчисления процессов, и я не очень хорошо понимаю это пространство; попробуйте прочитать несколько статей об этом самостоятельно и посмотреть, если вы получите какие-либо идеи.
После того, как я работал в Microsoft над Roslyn, я работал в Coverity, и одна из вещей, которые я сделал, - это получить интерфейс анализатора с использованием Roslyn. Благодаря точному лексическому, синтаксическому и семантическому анализу, предоставленному Microsoft, мы могли бы затем сосредоточиться на тяжелой работе по написанию детекторов, которые обнаружили общие проблемы многопоточности.
Фундаментальная причина, по которой у нас есть гонки и тупики и все такое, заключается в том, что мы пишем программы, которые говорят, что делать , и оказывается, что мы все дерьмо пишем императивные программы; компьютер делает то, что вы говорите, а мы говорим, чтобы он делал неправильные вещи. Многие современные языки программирования все больше и больше относятся к декларативному программированию: скажите, какие результаты вы хотите, и позвольте компилятору найти эффективный, безопасный и правильный способ достижения этого результата. Снова подумайте о LINQ; мы хотим, чтобы вы сказали from c in customers select c.FirstName
, что выражает намерение . Позвольте компилятору выяснить, как писать код.
Алгоритмы машинного обучения в некоторых задачах намного лучше, чем алгоритмы с ручным кодированием, хотя, конечно, есть много компромиссов, включая правильность, время, затраченное на обучение, ошибки, вызванные плохим обучением, и так далее. Но вполне вероятно, что очень многие задачи, которые мы в настоящее время кодируем «вручную», скоро станут доступными для машинно-генерируемых решений. Если люди не пишут код, они не пишут ошибки.
Извините, это было немного бессвязно; Это огромная и сложная тема, и за 20 лет, в течение которых я следил за прогрессом в этой проблемной области, в сообществе ЛП не было достигнуто четкого консенсуса.