Один мудрец однажды сказал:
Все, что может пойти не так, делает
— Мерфи
Некоторые программисты — мудрецы, поэтому мудрый программист однажды сказал:
Хороший программист — это тот, кто смотрит в обе стороны, прежде чем переходить улицу с односторонним движением.
В идеальном мире все работает как положено, и вы можете подумать, что это хорошая идея — продолжать потреблять вещи до конца. Таким образом, следующий шаблон встречается повсюду в каждой кодовой базе:
Джава
for (;;) { // something }
С
while (1) { // something }
Бейсик
10 something 20 GOTO 10
Хотите увидеть доказательства? Найдите github для while (true) и проверьте количество совпадений:
https://github.com/search?q=while+true&type=Code
Никогда не используйте бесконечные циклы
В информатике ведется очень интересная дискуссия на тему «Проблема остановки» . Суть проблемы остановки, доказанной Аланом Тьюрингом давно, заключается в том, что она действительно неразрешима. Хотя люди могут быстро оценить, что следующая программа никогда не остановится:
for (;;) continue;
… и что следующая программа всегда остановится:
for (;;) break;
… Компьютеры не могут определиться с такими вещами, и даже очень опытные люди могут не сразу сделать это, если взглянуть на более сложный алгоритм.
Обучение в процессе работы
В jOOQ мы недавно узнали о проблеме остановки трудным путем: действуя.
Перед исправлением проблемы № 3696 мы работали над ошибкой (или недостатком) в драйвере JDBC для SQL Server. Ошибка привела к тому, что SQLException
цепочки не сообщаются правильно, например, когда следующий триггер вызывает несколько ошибок:
CREATE TRIGGER Employee_Upd_2 ON EMPLOYEE FOR UPDATE AS BEGIN Raiserror('Employee_Upd_2 Trigger called...',16,-1) Raiserror('Employee_Upd_2 Trigger called...1',16,-1) Raiserror('Employee_Upd_2 Trigger called...2',16,-1) Raiserror('Employee_Upd_2 Trigger called...3',16,-1) Raiserror('Employee_Upd_2 Trigger called...4',16,-1) Raiserror('Employee_Upd_2 Trigger called...5',16,-1) END GO
Итак, мы явно использовали их SQLExceptions
, так что пользователи jOOQ получили одинаковое поведение для всех баз данных:
consumeLoop: for (;;) try { if (!stmt.getMoreResults() && stmt.getUpdateCount() == -1) break consumeLoop; } catch (SQLException e) { previous.setNextException(e); previous = e; }
Это сработало для большинства наших клиентов, так как цепочка исключений, о которых сообщалось, вероятно, конечна, а также, вероятно, довольно мала. Даже приведенный выше пример триггера не является реальным, поэтому число зарегистрированных ошибок может быть от 1 до 5.
Я только что сказал … «вероятно» ?
Как сказали наши первые мудрецы: число может быть от 1 до 5. Но это может быть и 1000. Или 1000000. Или хуже, бесконечное. Как и в случае проблемы # 3696 , когда клиент использовал jOOQ с SQL Azure. Таким образом, в идеальном мире не может быть бесконечно много SQLException
сообщений, но это не идеальный мир, и в SQL Azure также была ошибка (вероятно, все еще есть), которая снова и снова сообщала об одной и той же ошибке, что в конечном итоге приводило к OutOfMemoryError
Так как jOOQ создал огромную SQLException
цепочку, что, вероятно, лучше, чем бесконечный цикл. По крайней мере, исключение было легко обнаружить и обойти. Если цикл работал бесконечно, сервер мог быть полностью заблокирован для всех пользователей нашего клиента.
Исправление теперь по сути это:
consumeLoop: for (int i = 0; i < 256; i++) try { if (!stmt.getMoreResults() && stmt.getUpdateCount() == -1) break consumeLoop; } catch (SQLException e) { previous.setNextException(e); previous = e; }
Верно распространенному высказыванию:
640 КБ должно хватить на всех
Единственное исключение
Итак, как мы видели раньше, этот смущающий пример показывает, что все, что может пойти не так, делает . В связи с возможными бесконечными циклами, имейте в виду, что такого рода ошибки могут привести к поломке целых серверов.
Лаборатория реактивного движения в Калифорнийском технологическом институте сделала это важным правилом для своих стандартов кодирования :
Правило 3 (границы цикла)
Все циклы должны иметь статически определяемую верхнюю границу для максимального числа итераций цикла. Для инструмента статической проверки соответствия должна быть возможность подтвердить наличие привязки. Исключение допускается для использования одного бесконечного цикла для задачи или потока, где запросы принимаются и обрабатываются. Такой цикл сервера должен быть аннотирован комментарием C: / * @ non-terminating @ * /.
Таким образом, кроме очень немногих исключений, вы никогда не должны подвергать свой код риску бесконечных циклов, не предоставляя верхних границ итераций цикла (то же самое можно сказать и о рекурсии, кстати).
Вывод
Подойдите кодовую базу сегодня и искать любые возможные while (true)
, for (;;)
, do {} while (true);
и другие заявления. Внимательно изучите эти утверждения и посмотрите, могут ли они остановиться — например, используя break
, или throw
, или return
, или continue
(внешний цикл).
Скорее всего, вы или кто-то из вас, кто написал этот код, был таким же наивным, как и мы, полагая, что …
… да ладно, этого никогда не случится
Потому что вы знаете, что происходит, когда думаете, что ничего не произойдет .