понедельник, 4 февраля 2019 г.

[prog.c++] Небольшое послесловие про "Обедающих философов" и exception-safety

В статье про реализацию задачи про обедающих философов посредством Actors и CSP я отдельно затронул тему обеспечения exception-safety при использовании модели CSP. И, пожалуй, можно на этом моменте остановится подробнее еще раз.

Давайте представим себе, что мы захотели сделать функцию run_simulation() безопасной по отношению к исключениям.

Первое, что нам придется сделать -- это выполнить рекомендации из статьи по корректному завершению нитей для вилок. Т.е. сперва мы будем закрывать каналы, созданные для вилок, только потом будем вызывать join(). OK. С этим все понятно.

Далее нам нужно будет при возникновении каких-либо проблем завершить нити для философов.

И тут, если мы просто вызовем join(), мы опять наступим на те же грабли, не лежащие несколько по-другому. Дело в том, что внутри philosopher_process есть цикл, в котором выполняются вызовы receive(). Выход из receive() может произойти либо при получении ответа от вилки, либо при закрытии канала.

Но, если нити для вилок уже завершили свою работу, то вилки прислать ответ философу уже не смогут. И канал никто не закроет, т.к. каналом владеет сам philosopher_process, а run_simulation() к каналу доступа не получит.

Значит, каналы для философов мы так же должны создавать в run_simulation(), хранить их в контейнере, а потом принудительно закрывать прежде чем вызывать join() для нитей философов.

OK. Это уже шаг в верном направлении.

Допустим, что мы это сделали. Станет ли наше решение корректным?

К сожалению, нет. Т.к. внутри philosopher_process цикл с вызовами receive(). Из receive-то мы выйдем принудительно закрыв канал. А вот из цикла?

А из цикла мы не выйдем, т.к. для этого нужно увеличивать meals_eaten, а это происходит только при получении taken_t от правой вилки. Но ведь taken_t мы не получим, т.к. и вилки уже перестали работать, и наш канал уже закрыли.

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

В качестве сухого остатка повторю то, что говорил уже неоднократно: программировать многопоточность сложно. Особенно когда приходится работать с голыми нитями. Поэтому лично я предпочитаю использовать акторов, даже если это приводит к более многословным решениям. Все-таки при использовании акторов граблей немного поменьше.

Вот как мог бы выглядеть вариант philosopher_process, если бы пришлось делать безопасную к исключениям реализацию run_simulation(). Необходимые изменения выделены жирным.

void philosopher_process(
   trace_maker_t & tracer,
   so_5::mchain_t control_ch,
   so_5::mchain_t self_ch// Now it is created outside.
   std::size_t philosopher_index,
   so_5::mbox_t left_fork,
   so_5::mbox_t right_fork,
   int meals_count )
{
   int meals_eaten{ 0 };

   // This flag is necessary for tracing of philosopher actions.
   thinking_type_t thinking_type{ thinking_type_t::normal };

   random_pause_generator_t pause_generator;

   // New flag for cancelling work if a channel is closed.
   bool stopped = false;
   const auto on_close_handler = [&stopped](const auto &) { stopped = true; };

   // Now two conditions are checked.
   while( meals_eaten < meals_count && !stopped )
   {
      tracer.thinking_started( philosopher_index, thinking_type );

      // Simulate thinking by suspending the thread.
      std::this_thread::sleep_for( pause_generator.think_pause( thinking_type ) );

      // For the case if we can't take forks.
      thinking_type = thinking_type_t::hungry;

      // Try to get the left fork.
      tracer.take_left_attempt( philosopher_index );
      so_5::send< take_t >( left_fork, self_ch->as_mbox(), philosopher_index );

      // Request sent, wait for a reply.
      so_5::receive(
         // More complex condition for receive.
         so_5::from( self_ch ).handle_n( 1u ).on_close( on_close_handler ),
         []( so_5::mhood_t<busy_t> ) { /* nothing to do */ },
         [&]( so_5::mhood_t<taken_t> ) {
            // Left fork is taken.
            // Try to get the right fork.
            tracer.take_right_attempt( philosopher_index );
            so_5::send< take_t >(
                  right_fork, self_ch->as_mbox(), philosopher_index );

            // Request sent, wait for a reply.
            so_5::receive(
               // More complex condition for receive.
               so_5::from( self_ch ).handle_n( 1u ).on_close( on_close_handler ),
               []( so_5::mhood_t<busy_t> ) { /* nothing to do */ },
               [&]( so_5::mhood_t<taken_t> ) {
                  // Both fork are taken. We can eat.
                  tracer.eating_started( philosopher_index );

                  // Simulate eating by suspending the thread.
                  std::this_thread::sleep_for( pause_generator.eat_pause() );

                  // One step closer to the end.
                  ++meals_eaten;

                  // Right fork should be returned after eating.
                  so_5::send< put_t >( right_fork );

                  // Next thinking will be normal, not 'hungry_thinking'.
                  thinking_type = thinking_type_t::normal;
               } );

            // Left fork should be returned.
            so_5::send< put_t >( left_fork );
         } );

   }

   // Notify about the completion of the work.
   tracer.philosopher_done( philosopher_index );
   so_5::send< philosopher_done_t >( control_ch, philosopher_index );
}

2 комментария:

ssko комментирует...

Наверное, глупый вопрос от лени: а почему нельзя закрыть каналы к философам из нитей вилок при их прибитии?

eao197 комментирует...

@ssko
Вилки не хранят у себя каналы философов.