Статьи

Бесконечные циклы. Или: все, что может пойти не так, делает.

Один мудрец однажды сказал:

Все, что может пойти не так, делает

—  Мерфи

Некоторые программисты — мудрецы, поэтому мудрый программист однажды сказал:

Хороший программист — это тот, кто смотрит в обе стороны, прежде чем переходить улицу с односторонним движением.

—  Даг Линдер

В идеальном мире все работает как положено, и вы можете подумать, что это хорошая идея — продолжать потреблять вещи до конца. Таким образом, следующий шаблон встречается повсюду в каждой кодовой базе:

Джава

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 (внешний цикл).

Скорее всего, вы или кто-то из вас, кто написал этот код, был таким же наивным, как и мы, полагая, что …

… да ладно, этого никогда не случится

Потому что вы знаете, что происходит, когда думаете, что ничего не произойдет .