avva: (Default)
avva ([personal profile] avva) wrote2003-10-08 08:53 pm

интересный баг

А вот вчера обнаружился любопытный баг:

Есть программа synsuck.pl, которая скачивает с сети RSS-ленты, из которых строятся syndicated accounts. Она раньше была устроена примерно так:

- строим запрос к базе данных, вытягивающий данные об аккаунтах, к-е нужно обновить
- while(вытягиваем аккаунт из базы данных) {

делаем кучу всего для его обновления: скачиваем RSS-файл, парсим его с помощью XML-парсера, проверяем, появились ли там новые записи, вносим их в нашу базу данных

}

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

my $process_user = sub {

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

}

Потом идёт цикл, в котором главный процесс порождает детей с помощью fork, и каждый ребёнок вызывает функцию $process_user для своего аккаунта. Цикл выглядит примерно так (напомню для незнающих Perl, что "next;" в нём — то же, что "continue;" в C, переход к началу цикла):

while(есть, что делать) {
  
 if(my $pid = fork) {
    # я - отец
    записываем $pid сына в список работ
    next; # переход к началу цикла
 } else {
    # я - сын
    my $account = get_next_user();
    $process_user->($account);
    exit(0); # этот процесс заканчивает работу
 }
}


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

Мне пришлось с этим разбираться. Я довольно долго не понимал, что же, чёрт побери, происходит. Сделал специальную версию программы, в которой убрал всю RSS-логику, оставил только логику обработки детей (т.е. $process_user ничего не делает, только спит несколько секунд для отвода глаз) и кучу debug prints. Но в этой версии у меня не получалось повторить неправильное поведение. Я ещё немного потыкался и наконец разобрался.

Оказывается, в функции $process_user, ещё с тех времён, когда она была внутренностью большого цикла, осталась куча операторов "next;" в разных случаях, когда происходят какие-нибудь ошибки скачивания, или парсинга, или нет новых записей для базы данных. Но теперь этот код — не внутренность цикла, а часть функции, в которой никаких циклов нет; что же делают эти "next;"?

Они выводят управление за пределы функции и действуют на обволакивающий её вызов цикл в теле программы.

Т.е. в схеме, обрисованной выше, "next;" внутри вызова функции $process_user->($account) возвращает управление из функции и прыгает на начало главного цикла while(), избегая таким образом вызова exit(0); более того, сын начинает вследствие этого вести себя так, будто он — отец, и сам порождает нового сына, и так далее.

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

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 12:06 pm (UTC)(link)
Правильно ли я тебя понял, что в Пёрле для next - dynamic scoping вместо lexical scoping?

[identity profile] avva.livejournal.com 2003-10-08 12:17 pm (UTC)(link)
Судя по всему, да.

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 12:36 pm (UTC)(link)
Хороший пример того, почему виртуальная машина, заточенная под Си-шарп, будет плохо приспособленной даже к такому не очень необычному языку, как Пёрл.

[identity profile] dimrub.livejournal.com 2003-10-08 01:12 pm (UTC)(link)
Мне кажется, описанноое Аввой поведение не является примером, подтверждающим это утверждение. Компилятор перла над IL должен всего лишь учитывать этот факт, и вести себя соответственно. Возможно, mapping будет не очень очевидный, но что с того.

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 01:16 pm (UTC)(link)
т.е. для каждого цикла он должен записать Куда Следует, что вот, мы вошли в цикл, так, чтобы если субрутина, вызываемая из цикла, или субрутина, вызываемая из субрутины, вызываемой из цикла, захотела из этого цикла выйти, этот выход можно было бы произвести.

По-моему, это очень плохо накладывается на понятия .NET CLR.

[identity profile] dimrub.livejournal.com 2003-10-08 01:19 pm (UTC)(link)
Скорее наоборот, для каждого next он должен указать, к чему этот next относится (т.е. поставить implicit label).

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 01:26 pm (UTC)(link)
А если это статически нельзя определить, как в Аввыном примере?

[identity profile] dimrub.livejournal.com 2003-10-08 01:52 pm (UTC)(link)
Тэкс, что мы имеем? Раскручивание стэка до ближайшего контекста, находящегося в цикле? Я думаю, исключения идеально подходять для этой ситуации.

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 02:49 pm (UTC)(link)
то есть, вокруг каждого вызова субрутины в цикле -

try
{
...
}
catch (next_exception ne)
{
goto next_label;
}
catch (continue_excetion ce)
{
goto continue_label;
}
catch (last_exception be)
{
goto last_label;
}

[identity profile] dimrub.livejournal.com 2003-10-08 02:56 pm (UTC)(link)
Нет. Каждый цикл - внутри try block. А catch делает continue. Возможно, это не оптимальное решение, можно оптимизировать - вставлять такие блоки только вокруг тех циклов, в которых вызываются функции, из которых такое исключение может быть брошено (к сожалению, в отличие от джавы, в C# и CLR нет ключевого слова throws).

[identity profile] gdy.livejournal.com 2003-10-10 08:35 am (UTC)(link)
Chris Brumme про исключения в CLR.
However, there is a serious long term performance problem with exceptions and this must be factored into your decision.Consider some of the things that happen when you throw an exception. (http://blogs.gotdotnet.com/cbrumme/default.aspx?date=2003-10-01T00:00:00)

[identity profile] ex-ilyavinar899.livejournal.com 2003-10-08 06:25 pm (UTC)(link)
http://www.parrotcode.org/faq/#why_your_own_virtual_machine_why_not_compile_to_jvm/.net

Интересно, что это за various reasons.

[identity profile] xfyre.livejournal.com 2003-10-08 12:18 pm (UTC)(link)
все правильно: next в перле - это не оператор, а функция.
и действует она соответственно.

perldoc perlfunc, там много любопытного написано.
абсолютно та же фигня с last и continue :)

[identity profile] avva.livejournal.com 2003-10-08 12:23 pm (UTC)(link)
Ну вот в perlfunc написано:

"next" cannot be used to exit a block which returns a value
such as "eval {}", "sub {}" or "do {}", and should not be used
to exit a grep() or map() operation.

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

[identity profile] xfyre.livejournal.com 2003-10-08 12:34 pm (UTC)(link)
так она же не exit! она честно next внешний цикл! :)
только что специально проверил это на следующем простом примере:

#!/usr/bin/perl

use strict;
use vars qw($global_var $i);

$global_var = "before testSub()";
$i = 1;

while ( $i-- ) {
    &testSub();
}

print $global_var, "\n";

sub testSub {
    next;
    $global_var = "after testSub()";
}


угадайте с одного раза, что он выводит?

на самом деле, в перле полно подобных фишек. одна только имплементация try { ... } catch ... with { }; чего стоит.

[identity profile] avva.livejournal.com 2003-10-08 12:38 pm (UTC)(link)
Гм. У меня он выводит

$ perl 3.pl
before testSub()


т.е. вызов next; внутри testSub приводит к выходу из testSub, и
присвоение $global_var = "after testSub()"; не выполняется ;)

[identity profile] xfyre.livejournal.com 2003-10-08 12:43 pm (UTC)(link)
именно :)

а вот что про это сказано в perlsyn (найдено по той дискуссии, на которую давали ссылку ниже):


The while statement executes the block as long as the expression is true (does not evaluate to the null string "" or 0 or "0"). The LABEL is optional, and if present, consists of an identifier followed by a colon. The LABEL identifies the loop for the loop control statements next, last, and redo. If the LABEL is omitted, the loop control statement refers to the innermost enclosing loop. This may include dynamically looking back your call-stack at run time to find the LABEL. Such desperate behavior triggers a warning if you use the use warnings pragma or the -w flag. Unlike a foreach statement, a while statement never implicitly localises any variables.


т.е. это, ко всему прочему, documented behaviour, как и большинство других странностей перла :)

[identity profile] avva.livejournal.com 2003-10-08 12:47 pm (UTC)(link)
Да, я тоже это прочитал. Но согласитесь, что это противоречит процитированной выше документации next в perlfunc.
А т.к. именно документация next специально упоминает случай sub {}, то ей как бы больше веры должно быть, но она оказывается неверной ;)

[identity profile] xfyre.livejournal.com 2003-10-08 12:53 pm (UTC)(link)
с точки зрения здравого смысла - да, документация противоречива.

с формальной точки зрения - все правильно. попробуйте в вышеприведенном примере закомментировать цикл и оставить только вызов &testSub() - будет compile error ;)
nine_k: A stream of colors expanding from brain (Default)

[personal profile] nine_k 2003-10-09 01:24 am (UTC)(link)
>с точки зрения здравого смысла - да, документация противоречива.

True Perl spirit ;-]

[identity profile] saltovski.livejournal.com 2003-10-08 12:21 pm (UTC)(link)
А баг в стандарте или в самом интерпретаторе?

[identity profile] avva.livejournal.com 2003-10-08 12:24 pm (UTC)(link)
Баг в программе ;) т.е. эти next; должны были быть return; в любом случае.

Почему next; именно так себя ведёт, соответствует ли это официальному описанию языка, я ещё не разобрался.

[identity profile] darxeth.livejournal.com 2003-10-08 12:35 pm (UTC)(link)
Весьма странно и интересно. Я нашёл обсуждение (ссылка (http://groups.google.com/groups?hl=ru&lr=&ie=UTF-8&oe=UTF-8&threadm=WOa9OEO34SkcqBGgGZ7QInSS56hm%404ax.com&rnum=1&prev=/groups%3Fq%3Dnext%2Bsub%2Bperlfunc%26hl%3Dru%26lr%3D%26ie%3DUTF-8%26oe%3DUTF-8%26selm%3DWOa9OEO34SkcqBGgGZ7QInSS56hm%25404ax.com%26rnum%3D1)) этого вопроса в Google groups.
Не знаю насколько полезно, сам сейчас читаю.

(Anonymous) 2003-10-08 12:41 pm (UTC)(link)
Документация нам говорит: "next cannot be used to exit a block which returns a value such as eval {}, sub {} or do {}, and should not be used to exit a grep() or map() operation."

однако,


sub x{
next;
}
$i=1;
while($i<20){
$i++;
x();
print $i;
}


Что, конечно, печально

(Anonymous) 2003-10-08 12:42 pm (UTC)(link)
в смысле то, что оно ничего не напечатает

[identity profile] avva.livejournal.com 2003-10-08 12:45 pm (UTC)(link)
Вот-вот.

Правда, в perldoc perlsyn пишут, что

The LABEL identifies the loop for the loop control statements "next", "last", and "redo". If the LABEL is omitted, the loop control statement refers to the innermost enclosing loop. This may include dynamically looking back your call-stack at run time to find the LABEL. Such desperate behavior triggers a warning if you use the "use warnings" pragma or the -w flag.

И действительно, если запустить perl -w, то он говорит

Exiting subroutine via next at testsub.pl line 15.

Налицо противоречие в документации.

[identity profile] cema.livejournal.com 2003-10-08 12:52 pm (UTC)(link)
Потому что в Перле не функции, а подпрограммы. :-)

В сторону: что же теперь, все циклы метками помечать?

[identity profile] sobaker.livejournal.com 2003-10-08 11:27 pm (UTC)(link)
Т.е. continue выпрыгивает из функции?

Боже, какой дикий бред :)

[identity profile] gdy.livejournal.com 2003-10-10 08:55 am (UTC)(link)
Principle of Least Surprise (http://www.artima.com/intv/ruby4.html)
For example, I was a C++ programmer before I started designing Ruby. I programmed in C++ exclusively for two or three years. And after two years of C++ programming, it still surprised me.

[identity profile] sobaker.livejournal.com 2003-10-10 09:08 am (UTC)(link)
Кстати, да. С++ - громоздкий и сложный язык.

[identity profile] kot-begemot.livejournal.com 2003-10-10 06:18 pm (UTC)(link)
Сто двадцать пятое подтверждение того, что perl не следует использовать помимо элементарного парсинга, в каковой функции он не сильно превосходит awk. Вывод - perl - это никому не нужное извращение дилетанта, пользоваться которым просто опасно.