интересный баг
Oct. 8th, 2003 08:53 pmА вот вчера обнаружился любопытный баг:
Есть программа synsuck.pl, которая скачивает с сети RSS-ленты, из которых строятся syndicated accounts. Она раньше была устроена примерно так:
- строим запрос к базе данных, вытягивающий данные об аккаунтах, к-е нужно обновить
- while(вытягиваем аккаунт из базы данных) {
делаем кучу всего для его обновления: скачиваем RSS-файл, парсим его с помощью XML-парсера, проверяем, появились ли там новые записи, вносим их в нашу базу данных
}
Однако со временем этих аккаунтов стало так много, что программа элементарно перестала за ними поспевать. Поэтому несколько дней назад её сделали (не я) многопотоковой, после чего она стала выглядеть примерно так. Сначала идёт внутренняя функция:
my $process_user = sub {
эта функция принимает в качестве аргумента имя аккаунта, и делает то же, что делалось в большом
цикле в предыдущей версии, но только для этого аккаунта.
}
Потом идёт цикл, в котором главный процесс порождает детей с помощью fork, и каждый ребёнок вызывает функцию $process_user для своего аккаунта. Цикл выглядит примерно так (напомню для незнающих Perl, что "next;" в нём — то же, что "continue;" в C, переход к началу цикла):
В общем, всё это не работало. Дерево процессов, вместо одного отца и кучи созданных им детей, выглядело так, будто сыновья создавали, в свою очередь, других сыновей, что кажется совершенно невозможным согласно логике кода, ведь сразу после возвращения из процедуры обработки аккаунта каждый сын умирает вследствие вызова exit().
Мне пришлось с этим разбираться. Я довольно долго не понимал, что же, чёрт побери, происходит. Сделал специальную версию программы, в которой убрал всю RSS-логику, оставил только логику обработки детей (т.е. $process_user ничего не делает, только спит несколько секунд для отвода глаз) и кучу debug prints. Но в этой версии у меня не получалось повторить неправильное поведение. Я ещё немного потыкался и наконец разобрался.
Оказывается, в функции $process_user, ещё с тех времён, когда она была внутренностью большого цикла, осталась куча операторов "next;" в разных случаях, когда происходят какие-нибудь ошибки скачивания, или парсинга, или нет новых записей для базы данных. Но теперь этот код — не внутренность цикла, а часть функции, в которой никаких циклов нет; что же делают эти "next;"?
Они выводят управление за пределы функции и действуют на обволакивающий её вызов цикл в теле программы.
Т.е. в схеме, обрисованной выше, "next;" внутри вызова функции $process_user->($account) возвращает управление из функции и прыгает на начало главного цикла while(), избегая таким образом вызова exit(0); более того, сын начинает вследствие этого вести себя так, будто он — отец, и сам порождает нового сына, и так далее.
Я понятия не имел, что такое вообще может произойти, и это меня, мягко говоря, сильно удивило.
Есть программа 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); более того, сын начинает вследствие этого вести себя так, будто он — отец, и сам порождает нового сына, и так далее.
Я понятия не имел, что такое вообще может произойти, и это меня, мягко говоря, сильно удивило.
no subject
Date: 2003-10-08 12:06 pm (UTC)no subject
Date: 2003-10-08 12:17 pm (UTC)no subject
Date: 2003-10-08 12:18 pm (UTC)и действует она соответственно.
perldoc perlfunc, там много любопытного написано.
абсолютно та же фигня с last и continue :)
no subject
Date: 2003-10-08 12:21 pm (UTC)no subject
Date: 2003-10-08 12:23 pm (UTC)"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.
чего я и ожидал, собственно. Но на практике выходит по-другому.
no subject
Date: 2003-10-08 12:24 pm (UTC)Почему next; именно так себя ведёт, соответствует ли это официальному описанию языка, я ещё не разобрался.
no subject
Date: 2003-10-08 12:34 pm (UTC)только что специально проверил это на следующем простом примере:
#!/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 { }; чего стоит.
no subject
Date: 2003-10-08 12:35 pm (UTC)Не знаю насколько полезно, сам сейчас читаю.
no subject
Date: 2003-10-08 12:36 pm (UTC)no subject
Date: 2003-10-08 12:38 pm (UTC)т.е. вызов next; внутри testSub приводит к выходу из testSub, и
присвоение $global_var = "after testSub()"; не выполняется ;)
no subject
Date: 2003-10-08 12:41 pm (UTC)однако,
sub x{
next;
}
$i=1;
while($i<20){
$i++;
x();
print $i;
}
Что, конечно, печально
no subject
Date: 2003-10-08 12:42 pm (UTC)no subject
Date: 2003-10-08 12:43 pm (UTC)а вот что про это сказано в 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, как и большинство других странностей перла :)
no subject
Date: 2003-10-08 12:45 pm (UTC)Правда, в 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.
Налицо противоречие в документации.
no subject
Date: 2003-10-08 12:47 pm (UTC)А т.к. именно документация next специально упоминает случай sub {}, то ей как бы больше веры должно быть, но она оказывается неверной ;)
no subject
Date: 2003-10-08 12:52 pm (UTC)В сторону: что же теперь, все циклы метками помечать?
no subject
Date: 2003-10-08 12:53 pm (UTC)с формальной точки зрения - все правильно. попробуйте в вышеприведенном примере закомментировать цикл и оставить только вызов &testSub() - будет compile error ;)
no subject
Date: 2003-10-08 01:12 pm (UTC)no subject
Date: 2003-10-08 01:16 pm (UTC)По-моему, это очень плохо накладывается на понятия .NET CLR.
no subject
Date: 2003-10-08 01:19 pm (UTC)no subject
Date: 2003-10-08 01:26 pm (UTC)no subject
Date: 2003-10-08 01:52 pm (UTC)no subject
Date: 2003-10-08 02:49 pm (UTC)try
{
...
}
catch (next_exception ne)
{
goto next_label;
}
catch (continue_excetion ce)
{
goto continue_label;
}
catch (last_exception be)
{
goto last_label;
}
no subject
Date: 2003-10-08 02:56 pm (UTC)no subject
Date: 2003-10-08 06:25 pm (UTC)Интересно, что это за various reasons.
no subject
Date: 2003-10-08 11:27 pm (UTC)Боже, какой дикий бред :)
no subject
True Perl spirit ;-]
no subject
Date: 2003-10-10 08:35 am (UTC)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)
no subject
Date: 2003-10-10 08:55 am (UTC)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.
no subject
Date: 2003-10-10 09:08 am (UTC)no subject
Date: 2003-10-10 06:18 pm (UTC)