Задачи в Rust. Подводные камни.

Все написанное в данной заметке актуально для компилятора Rust версии 0.7

Более-менее глубокой информации о задачах в Rust не много, поэтому, для того что бы разобраться в том, как же это работает, приходится экспериментировать “на кошках”. Посидев некоторое время за исходниками Rust, я открыл для себя много нового.
Самое неожиданное: на данный момент планировщиков задач аж два! Один старый, более функциональный, правда, на данный момент находящийся в довольно непотребном состоянии и новый, который еще не дописан. Подобная ситуация приводит к тому, что поведение задач, запущенных при помощи методов TaskBuilder отличаются от поведения задач, запущенных при помощи функций из модуля task.
Ну а после “оптимистичного” введения, небольшой рассказ о планировщиках Rust и том, как с ними работать на данный момент.

Старый планировщик

Если зайти на сайт Rust, то в разделе посвященном модулю Task можно найти информацию о ряде доступных типах планировщиков, заявленное поведение которых расходится с реальным:

  • DefaultScheduler – создается по одному OS потоку на ядро процессора.
  • PlatformThread – все задачи выполняются в одном потоке последовательно.
  • SingleThreaded, ThreadPerCore, ManualThreads(x) – создаются по одному OS потоку на задачу.
  • ThreadPerTask – при попытке содать планировщик этого типа вылетает исключение. Загадочно, не правда ли?

Как видно, поведение планировщиков совершенно не соответствует их названиям, а задачи запускаемые на старых планировщиках игнорируют флаги supervised, unlinked, что делает управление жизнью дерева задач не возможным.
В принципе, я надеюсь что данные ошибки будут исправлены, так как работать с задачами с использованием специально настроенных планировщиков было довольно удобно:

let mut schld = task::task();              // (1)
schld.sched_mode(task::DefaultScheduler);  // (2)
schld.unlinked();                          // (3)
do shd.spawn {                             // (4)
    to_do_sometnig();
}

Для того, что бы иметь возможность не только указать желаемый планировщик, но и настроить его параметры, необходимо обзавестись объектом TaskBuilder (1). После того, как объект TaskBuilder получен, его можно сконфигурировать, например, указав ему какой планировщик необходимо использовать (2) и задать флаг unlinked (3), который в недрах runtime будет проигнорирован. А жаль, ведь этот флаг очень полезен, так как позволяет указать на необходимость продолжить работу задачи родителя, в случае аварийной остановки дочерней задачи. После того, как планировщик сконфигурирован (а флагов у него не много), можно запустить задачу на выполнение (4).

Новый планировщик

На данный момент, новый планировщик используется вместе с наиболее распространенным синтаксисом запуска задач, использующим функции из модуля task: do spawn { … }, т.е. без предварительного конструирования TaskBuilder. Так, эквивалентным для первого варианта кодом, использующим новый планировщик будет:

do task::spawn {
    to_do_sometnig();
}

Но, в отличие от примера со старым планировщиком, задача вполне может быть запущена в unlinked режиме:

do task::spawn_unlinked {  // (1)
    to_do_sometnig();
    fail!();           // (2)
}

В этом примере, вызов функции fail!(); (2) вызовет аварийное завершение только дочерней задачи, в то время как родительская задача продолжит свою работу, так как задача были изначально запущена (1) с флагом unlinked.

Тестовый код и отладочный вывод

Для тестирования я написал небольшую програмку, доступную тут, основной код которой выглядит следующим образом:

for uint::range(0, self.tasks_count) |i| {
    io::println(fmt!("Spawning #%u", i));

    let mut shd = task::task();
    shd.sched_mode(self.mode);
    let timeout = self.timeout as u32;
    do shd.spawn {
        io::println(fmt!("Before sleeping #%u", i));
        unsafe{ libc::sleep(timeout); }
        io::println(fmt!("Waked up #%u", i));
    }
}

Отладочный вывод получился информативным. Для планировщика по умолчанию, который де-факто является ThreadPerCore планировщиком, он выглядит следующим образом:

==========DefaultScheduler===========
Spawning #0
Spawning #1
Before sleeping #0
Spawning #2
Before sleeping #1
Spawning #3
Spawning #4
Spawning #5
Spawning #6
Before sleeping #2
Spawning #7
Spawning #8
Spawning #9
Spawning #10
Spawning #11
Before sleeping #7
Waked up #7
Waked up #1
Waked up #2
Waked up #0
Before sleeping #11
Before sleeping #8
Before sleeping #5
Before sleeping #10
Waked up #10
Waked up #11
Waked up #8
Waked up #5
Before sleeping #6
Before sleeping #4
Before sleeping #9
Before sleeping #3
Waked up #6
Waked up #3
Waked up #9
Waked up #4

Т.е. все задачи отправились на выполнение, но потоков всего 4, поэтому, задачи выводят сообщения о начале своей работы по 4 подряд.
Если отладочный вывод планировщика по умолчанию заставляет хоть немного задуматься, что в случае с планировщиком PlatformThread всё предельно ясно. Все задачи отправляются на выполнение по очереди, хотя принципа fifo не наблюдается, но его никто и не обещал:

==========PlatformThread===========
Spawning #0
Spawning #1
Spawning #2
Spawning #3
Spawning #4
Spawning #5
Spawning #6
Before sleeping #0
Spawning #7
Spawning #8
Spawning #9
Spawning #10
Spawning #11
Waked up #0
Before sleeping #7
Waked up #7
Before sleeping #8
Waked up #8
Before sleeping #1
Waked up #1
Before sleeping #10
Waked up #10
Before sleeping #9
Waked up #9
Before sleeping #3
Waked up #3
Before sleeping #2
Waked up #2
Before sleeping #11
Waked up #11
Before sleeping #6
Waked up #6
Before sleeping #5
Waked up #5
Before sleeping #4
Waked up #4

У всех остальных планировщиков вывод будет приблизительно одинаковый, с различиями лишь в том, какая из задач первой отправилась на выполнение:

==========ManualThreads(1)===========
Spawning #0
Spawning #1
Before sleeping #0
Spawning #2
Before sleeping #1
Spawning #3
Before sleeping #2
Spawning #4
Before sleeping #3
Spawning #5
Before sleeping #4
Spawning #6
Before sleeping #5
Spawning #7
Before sleeping #6
Spawning #8
Before sleeping #7
Spawning #9
Before sleeping #8
Spawning #10
Before sleeping #9
Spawning #11
Before sleeping #10
Before sleeping #11
Waked up #0
Waked up #3
Waked up #2
Waked up #1
Waked up #4
Waked up #5
Waked up #6
Waked up #7
Waked up #8
Waked up #9
Waked up #11
Waked up #10

Leave a Reply