Написание многопоточного кода, защищенного от исключений
-
11-07-2019 - |
Вопрос
Каковы противоречия между многопоточностью и безопасностью исключений в C++?Существуют ли хорошие рекомендации, которым следует следовать?Завершается ли поток из-за неперехваченного исключения?
Решение
Я полагаю, что стандарт C ++ не упоминает о многопоточности - многопоточность - это особенность платформы.
Я не совсем уверен, что стандарт C ++ говорит о необработанных исключениях в целом, но, согласно на этой странице , что происходит, определяется платформой, и вы должны узнать об этом в документации вашего компилятора.
В быстром и грязном тесте, который я сделал с g ++ 4.0.1 (точнее, i686-apple-darwin8-g ++ - 4.0.1), результат terminate ()
называется, который убивает всю программу. Код, который я использовал, следующий:
#include <stdio.h>
#include <pthread.h>
void *threadproc(void *x)
{
throw 0;
return NULL;
}
int main(int argc, char **argv)
{
pthread_t t;
pthread_create(&t, NULL, threadproc, NULL);
void *ret;
pthread_join(t, &ret);
printf("ret = 0x%08x\n", ret);
return 0;
}
Скомпилировано с помощью g ++ threadtest.cc -lpthread -o threadtest
. Вывод был:
terminate called after throwing an instance of 'int'
Другие советы
C ++ 0x будет иметь язык Поддержка транспортировки исключений между потоками , чтобы при возникновении исключения рабочий поток порождал поток, который мог его перехватить или перебросить.
Из предложения:
namespace std {
typedef unspecified exception_ptr;
exception_ptr current_exception();
void rethrow_exception( exception_ptr p );
template< class E > exception_ptr copy_exception( E e );
}
Неполученное исключение вызовет terminate ()
, который, в свою очередь, вызывает terminate_handler
(который может быть установлен программой). По умолчанию terminate_handler
вызывает abort ()
.
Даже если вы переопределяете terminate_handler по умолчанию
по умолчанию, стандарт говорит, что подпрограмма, которую вы предоставляете " должна прервать выполнение программы, не возвращаясь к вызывающей стороне " (ISO 14882-2003 18.6.1.3).
Итак, в итоге, необработанное исключение завершит программу, а не только поток.
Что касается безопасности потоков, Адам Розенфилд говорит, что специфичная для платформы вещь, которая не рассматривается стандартом.
Это единственная главная причина существования Эрланга.
Я не знаю, что такое конвенция, но имхо, будь как Erlang-подобным, насколько это возможно. Сделайте объекты кучи неизменными и настройте некоторый протокол передачи сообщений для связи между потоками. Избегайте замков. Убедитесь, что передача сообщений исключительна. Храните как можно больше содержимого в стеке.
Как уже обсуждали другие, параллелизм (и в особенности безопасность потоков) - это архитектурная проблема, которая влияет на то, как вы проектируете свою систему и свое приложение.
Но я хотел бы ответить на ваш вопрос о напряженности между безопасностью исключений и безопасностью потоков.
На уровне класса потокобезопасность требует изменений в интерфейсе. Так же, как исключение безопасности. Например, классы обычно возвращают ссылки на внутренние переменные, например:
class Foo {
public:
void set_value(std::string const & s);
std::string const & value() const;
};
Если Foo используется несколькими потоками, вас ждут проблемы. Естественно, вы можете поставить мьютекс или другую блокировку для доступа к Foo. Но довольно скоро все программисты на C ++ захотят заключить Foo в «ThreadSafeFoo». Я утверждаю, что интерфейс для Foo должен быть изменен на:
class Foo {
public:
void set_value(std::string const & s);
std::string value() const;
};
Да, это дороже, но его можно сделать поточно-ориентированным с помощью замков внутри Foo. IMnsHO это создает определенное напряжение между безопасностью потоков и безопасностью исключений. Или, по крайней мере, вам нужно провести дополнительный анализ, поскольку каждый класс, используемый в качестве общего ресурса, должен быть исследован в обоих источниках.
Один классический пример (не помню, где я его впервые увидел) - в библиотеке std.
Вот как вы извлекаете что-то из очереди:
T t;
t = q.front(); // may throw
q.pop();
Этот интерфейс несколько тупой по сравнению с:
T t = q.pop();
Но это сделано, потому что T-копия может бросить. Если копия выбрасывается после появления всплывающего окна, этот элемент теряется из очереди и не может быть восстановлен. Но поскольку копирование происходит до извлечения элемента, вы можете поместить произвольную обработку вокруг копии из front () в блоки try / catch.
Недостатком является то, что вы не можете реализовать очередь, которая является поточно-ориентированной, с интерфейсом std :: queue из-за двух задействованных шагов. То, что хорошо для безопасности исключений (разделяя шаги, которые можно выполнить), теперь плохо для многопоточности.
Ваш главный спаситель в безопасности исключений - это то, что операции с указателями не являются броском. Аналогично, операции с указателями можно сделать атомарными на большинстве платформ, поэтому они могут быть вашим спасителем в многопоточном коде. Вы можете съесть свой торт и съесть его, но это действительно сложно.
Я заметил две проблемы:
в g++ в Linux уничтожение потока (pthread_cancel) осуществляется путем создания «неизвестного» исключения.С одной стороны, это позволяет вам хорошо очистить поток при уничтожении.С другой стороны, если вы перехватите это исключение и не создадите его повторно, ваш код завершится функцией abort().Следовательно, если вы или какая-либо из библиотек, которые вы используете, уничтожаете потоки, вы не можете
ловить(...)
без
throw;
в вашем резьбовом коде. Здесь является ссылкой на такое поведение в сети:
- Иногда вам нужно передать исключение между потоками.Это непростая задача — в конечном итоге мы применили кое-какой хак, когда правильным решением является тот тип маршаллинга/демаршаллинга, который вы используете между процессами.
Я не рекомендую оставлять какое-либо исключение невредимым. Оберните ваши потоковые функции верхнего уровня в обработчики catch-all, которые могут более изящно (или, по крайней мере, многословно) завершить работу программы.
Я думаю, что самое важное - помнить, что неперехваченные исключения из других потоков не показываются пользователю и не генерируются в основном потоке. Таким образом, вы должны деформировать весь код, который должен выполняться в потоках, отличных от основного потока, с помощью блоков try / catch.