Раз уж на неделе затронул вопрос осторожного обращения с голыми нитями, то можно осветить еще один момент. Касающийся обеспечения exception safety при работе с многопоточностью.
В качестве демонстрации буду использовать вот такую небольшую программку:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std; enum class command { wait, calculate, close }; struct context { mutex m_lock; condition_variable m_cond; command m_command; }; void worker( context & ctx ) { command cmd = command::wait; do { { unique_lock< mutex > lock{ ctx.m_lock }; if( command::wait == ctx.m_command ) ctx.m_cond.wait( lock, [&ctx] { return command::wait != ctx.m_command; } ); cmd = ctx.m_command; ctx.m_command = command::wait; } } while( cmd != command::close ); cout << "worker closed" << endl; } void demo() { context first_ctx; thread first_thread{ worker, ref(first_ctx) }; { lock_guard< mutex > lock{ first_ctx.m_lock }; first_ctx.m_command = command::close; first_ctx.m_cond.notify_one(); } first_thread.join(); } int main() { try { demo(); } catch( const exception & x ) { cerr << "Oops: " << x.what() << endl; } } |
Здесь функция demo создает рабочий поток, в который предполагается выдавать некоторые команды. Рабочий поток должен ждать получения очередной команды, выполнять ее, ждать следующей команды и т.д. Рабочий поток завершает свою работу когда получает команду command::close. Для взаимодействия потоков используется стандартная пара из mutex-а и condition_variable. Пока команды для рабочего потока нет, он спит на условной переменной.
Функции worker() и demo() не выполняют никакой полезной работы. Это просто "рыба", заготовка, которая демонстрирует общий принцип. Поэтому demo() просто стартует рабочий поток, затем просто отдает ему команду command::close, дожидается завершения рабочего потока и возвращает управление функции main().
Теперь представим себе, что мы начали наполнять demo() реальными операциями. Т.к. исключения у нас не запрещены, то нужно быть готовым к тому, что где-то после старта рабочего потока какое-то исключение у нас выскочит. Готовы ли мы к этому? Проверим, добавив в код явное выбрасывание исключения:
void demo() { context first_ctx; thread first_thread{ worker, ref(first_ctx) }; throw runtime_error{ "something wrong!" }; { lock_guard< mutex > lock{ first_ctx.m_lock }; first_ctx.m_command = command::close; first_ctx.m_cond.notify_one(); } first_thread.join(); } |
Что мы получим при запуске? Крах приложения. Поскольку до first_worker.join() дело не дойдет.
Казалось бы, в чем проблема? Напишем RAII-обертку, которая будет для нас вызывать join для запущенных нами потоков:
class auto_joiner { thread & m_what; public : auto_joiner( thread & what ) : m_what{ what } {} ~auto_joiner() { m_what.join(); } }; ... void demo() { context first_ctx; thread first_thread{ worker, ref(first_ctx) }; auto_joiner first_joiner{ first_thread }; throw runtime_error{ "something wrong!" }; { lock_guard< mutex > lock{ first_ctx.m_lock }; first_ctx.m_command = command::close; first_ctx.m_cond.notify_one(); } } |
Запускаем и что получим? Зависание!
Это потому, что сигнала на завершение нашей рабочей нити дано не было. И рабочая нить продолжает висеть на условной переменной. А функция demo() висит на join()-е. Классический тупик.
В результате нужно сделать еще одну RAII обертку, для того, чтобы при раскрутке стека выдать команды на завершение рабочей нити:
void demo() { context first_ctx; thread first_thread{ worker, ref(first_ctx) }; auto_joiner first_joiner{ first_thread }; class notificator_type { context & m_ctx; public : notificator_type( context & ctx ) : m_ctx{ ctx } {} ~notificator_type() { lock_guard< mutex > lock{ m_ctx.m_lock }; m_ctx.m_command = command::close; m_ctx.m_cond.notify_one(); } } first_notificator{ first_ctx }; throw runtime_error{ "something wrong!" }; { lock_guard< mutex > lock{ first_ctx.m_lock }; first_ctx.m_command = command::close; first_ctx.m_cond.notify_one(); } } |
Вот теперь все завершается как положено.
Но ведь усилий для этого довелось приложить не так уж и мало, не правда ли? И это только для одного рабочего потока. А если бы их было не один, не два и даже не три?
В общем, многопоточность -- дело сложное. Лучше держаться от нее подальше, тем более, что на современной технике простой однопоточный код можеть показать лучшую производительность, что перемудренный многопоточный. А если уж приходится использовать многопоточность, то сперва имеет смысл воспользоваться чужими готовыми инструментами. Пусть у их разработчиков голова болит о том, где, что и как может пойти не так. И лишь в крайнем случае браться за ручное управление голыми потоками вручную.
Комментариев нет:
Отправить комментарий