Упражнения

1. Метод add-dependency в классе Task позволяет создавать циклические зависимости в графе зависимостей между задачами. Это значит, что если вы будете продвигаться по ссылкам между задачами, то вернетесь к исходной. Таким образом образуется бесконечный цикл и метод perform класса Task никогда не завершиться. Покажите как создать такой граф.

Ответ: Вы можете создать две задачи, а затем "замкнуть" их с помощью метода add-dependency следующим образом:

    my $a = Task.new({ say 'A' });
    my $b = Task.new({ say 'B' }, $a);
    $a.add-dependency($b);

Метод perform никогда не завершиться, потому что он первым делом будет вызывать perlform своих зависимостей. А так как $a и $b зависят друг от друга, никто из них не завершить вызовы callbacks. Программа завершится вследствие нехватки памяти, так и не напечатая на экране 'A' or 'B'.

2. Есть ли способ определить наличие цикла во время вызова perform? Есть ли способ предотвратить появление такого цикла с помощью add-dependency?

Answer: Чтобы обнаружить повторный вызов метода perform во время его выполнения, достаточно дополнить состояние объекта статусом старта вызова метода perform:

    augment class Task {
        has Bool $!started = False;

        method perform() {
            if $!started++ && !$!done {
                die "Cycle detected, aborting";
            }

            unless $!done {
                .perform() for @!dependencies;
                &!callback();
                $!done = True;
            }
        }
    }

Еще один способ избежать циклических вызовов - это во время вызова метода add-dependency проверять добавляемые объекты на предмет присутствия их в зависимостях уже добавленных объектов ( собственно это и есть причина циклических вызовов). Данное решение потребует создание вспомогательного метода depends-on, который проверяет находится ли указанная задача в зависимостях напрямую или транзитивно. Обратите внимание как используются » и [||], чтобы код обхода зависимостей получился эффективным и лаконичным:

    augment class Task {
        method depends-on(Task $some-task) {
            $some-task === any(@!dependencies)
            [||] @!dependencies».depends-on($some-task)
        }

        method add-dependency(Task $dependency) {
            if $dependency.depends-on(self) {
                warn 'Cannot add that task, since it would introduce a cycle.';
                return;
            }
            push @!dependencies, $dependency;
        }
    }

3. Каким образом объект Task может выполнит зависимые задачи параллельно ? (Подумайте как можно избежать коллизий в "бриллиантовых зависимостях" (Пер. - не встречал ранее такого выражения.)), когда две разных зависимых задачи требуют выполнения одной). Ответ: Включение параллельного выполнения просто: достаточно заменить вызов метода .perform() для @!dependencies; на @!dependencies».perform(). Однако в таком случае могут возникнуть "гонки" I(race conditions) в случае наличия бриллиантовых зависимостей, когда задача A запускает одновременно B и C, а они в свою очередь запускают D (D запускается дважды). Решение этой проблемы такое же как и в случае с циклическими вызовами в вопросе 2 - ввести атрибут $!started. Заметьте, что в случае параллельного выполнения, вызов die в одной из задач может прервать исполнение других.

    augment class Task {
        has Bool $!started = False;

        method perform() {
            unless $!started++ {
                @!dependencies».perform();
                &!callback();
                $!done = True;
            }
        }
    }