Комментарии 40
Думаю это зависит от задачи, но пойманное исключение в конкретном потоке можно записать в расшаренный std::excertion_ptr и в ожидающем потоке прокинуть его через std::rethrow_exception
Разбор всегда в одном потоке. Но он использует глобальные таблицы адресов, которые могут меняться в момент загрузки или выгрузки динамических библиотек (другими потоками). Чтобы это не вызывало проблем, текущая реализация берет глобальный лок, из-за чего исключения на всех потоках сериализуются - исключения в других потоках тормозят обработку исключений вашим.
Исправление текущей ситуации о котором они пишут - заменить глобальный лок на read-write lock, брать его на запись только при операциях с shared library, но это ломает ABI.
Имеется в виду, что в структуре данных где мы держим foo, bar, и baz, могут появляться или исчезать элементы. Наши foo, bar, и baz не меняются, но структура данных - меняется. Тупой пример: у вас есть std::vector<>, добавление элементов в него может привести к релокации данных, ломая итераторы в других потоках. Это надо защищать синхронизацией, текущая реализация везде довольно примитивная.
Вам стоит самые базы синхронизации почитать где-нибудь. Представьте используете вы глобальный массив arr
известных адресов функций, и пишите вы код для поиска в нем вроде:
for (int i = 0; i < arr.size(); ++i) {
auto* p = arr[i];
...
}
И вот в середине этого цикла другой поток скажем удаляет элемент из массива, после того как i
мы уже сравнили с длиной массива, но до того как прочитали arr[i]
- мы можем прочитать за границей массива. Или мы прочитали первые 10 элементов, а элементы с 5 по 10 были только что удалены, на их место попали элементы с 11 по 16, а мы эти индексы уже пропустили и эти элементы не увидим. Или другой поток добавляет что-то, arr
уже реалоцировали в новое место, но в него еще не скопировали старые данные - просто прочитаем мусор. Да что угодно может быть без синхронизации.
Исключения не должны добавлять никакого оверхеда вообще если исключения не кидаются. Поэтому никакого локального arr
на поток нет, при вызовах никуда не помещают никаких адресов, кроме обычного стека вызовов.
Есть глобальная таблица всех функций, которые в принципе участвуют в раскрутке стека, и при раскрутке адреса из стека ищутся в этой таблице. Вот эту глобальную таблицу и надо защищать.
Либо сделать (иной) lockfree доступ к этой таблице?Ничего не мешает, об этом в статье также написано. Вот только компиляторы уже сделали так как сделали. Для переделки нужно перекомпилировать все существующие с++ бинарники.
В принципе можно было бы сделать включение этого режима через какой-то флаг и переправить ответственность за abi на клиентов, тем кому это реально нужно.
Ключевой вопрос тут: а как вы найдете где находится обработчик исключения? Причем место обработчика нужно знать уже в моменте выброса исключения (иначе может статься, что обработчика нет, а мы уже весь стек до __start() раскрутили и потеряли весь контекст где произошла ошибка. И все дампы теперь бесполезны). Тут нужны какие-то таблицы сопоставляющие адреса функций и обработчики. И при раскрутке стека ходить по этим таблицам. А эти таблицы как раз могут меняться в добавлением / удалением функций (загрузке / выгрузке шареных либ, а может ещё в каких-то случаях).
Ещё есть таблицы «всех функций» при компилировании c -fomit-frame-pointer. Иначе без таблицы просто нельзя будет получить текущий стек (ну ладно, можно анализируя код, но это будет очень долго). Они, возможно, тоже блокируются при подобных операциях.
Таблица чтобы обработать свертку стека (деструкторы) по пути от исключения до обработчика. Ну и чтобы обработчик найти
Ядром - да, но тут мне пришла аналогия из Пиратов Карибского Моря.
"Всем ни с места! Я обронил мозги."
И что-то похожее происходит во время исключений в... да практически во всех ЯП.
Если исключения случились в других потоках, то часть тредов повисает в ожидании анвиндинга из-за глобального лока, как минимум. И речь не о разборе, а именно о бросании исключения.
самых распространённых машин скоро будет 256 и более ядер
Такое количество ядер мигом упрётся огромное количество каналов памяти, которые необходимы что бы обеспечить подвод и отвод данных которые эти ядра должны обрабатывать.
И даже если вы не уполномочены завершить программу целиком (например это контроллер тормозов какой-то или сервер критичный) — нужно завершать поток выполнения если причина выкидывания исключений неустранима. Тогда есть шанс хотя бы что устройство свалится в какой-то safe-mode или direct-control, или в другую контролируемую деградацию. Или хотя бы у человека лампочка красная высветится об отказе… А если вы стреляете исключениями в 1% случаев выполнения и продолжаете — это уже деградация неконтролируемая и невидимая. И потому — вдвойне опасная. И падение производительности в такой ситуации может оказаться наименьшей из возникающих проблем…
И есть много ошибок, для устранения которых не нужно ничего делать, просто вернуть эту ошибку пользователю и продолжить работу. Но кроме исключений стандартная библиотека не дает особо вариантов.
ps. Есть конечно вариант писать код так, чтобы исключений не возникало, ну то есть, например, запретить использовать std::map::at и всегда искать через std::map::find. И так для каждого контейнера. Но без автоматических проверок, такой способ, к сожалению, не скейлится =(
исключения, как способ сообщить об ошибке сейчас принято как дефолтное поведение
smthWrongWithHumans : public (std::exceptions) {
public:
smthWrongWithHumans(int count):humanCount(count) {};
...
private:
int humanCount{0};
}
//где-то в коде без исключений
void newShelterPopulationInfoRcv(int humansCount) {
sendNewFoodDistribution(TotalFood/humansCount);
//что делать если людь закончился
}
//где-то в коде с проверкой
errorCode newShelterPopulationInfoRcv(int humansCount) {
if (humansCount = 0) {
return errorCode::needNewHumans;
}
if (humansCount < 0) {
return errorCode::needNewHumans &
errorCode::FoodAttacs &
errorCode::needSweepTeam;
}
sendNewFoodDistribution(TotalFood/humansCount);
return errorCode::JustSmileAndWave;
}
//а теперь на исключениях
void newShelterPopulationInfoRcv(int humansCount) {
if (humansCount <= 0) {
throw smthWrongWithHumans(humansCount);
// пусть братва из вышележащего сервиса разирается
return;
}
sendNewFoodDistribution(TotalFood/humansCount);
}
smthWrongWithHumans : public (std::exceptions) {
public:
smthWrongWithHumans(int count):humanCount(count) {};
...
private:
int humanCount{0};
}
...
void newShelterPopulationInfoRcv(int humansCount) {
if (humansCount <= 0) {
throw smthWrongWithHumans(humansCount);
// пусть братва из вышележащего сервиса разирается
return;
}
sendNewFoodDistribution(TotalFood/humansCount);
}
Зачем шило заворачивать в капусту?
Можно проще, привести ЯВНО переменную к типу int
Вот так throw (int)humansCount;
Просто укажите, что у вас исключения целочисленного типа.
Я делаю так:
int findtext(const wchar_t* file, const wchar_t* text)
{
FILE* hfile = nullptr;
wchar_t* memptr = nullptr;
intptr_t hfind = 0;
_finddata_t data;
_fsize_t& size = data.size;
int retval = 0;
try
{
if ((file == nullptr) && (text == nullptr)) throw (int)EINVAL;
hfind = _findfirst(file, &data);
if (hfind == 0) throw (int)ENFILE;
_findclose(hfind);
hfind = 0;
memptr = (wchar_t*)malloc(size);
if (memptr == nullptr) throw (int)ENOMEM;
hfile = fopen(file, "rt");
if (hfile == nullptr) throw (int)EACCES;
fread(memptr, sizeof(wchar_t), size, hfile);
fclose(hfile);
hfile = nullptr;
retval = _memfind(memptr, text);
}
catch (int err)
{
_set_errno(err);
};
if (hfile) { fclose(hfile); hfile = nullptr; };
if (memptr) { memset(memptr, '/0', _msize(memptr)); free(memptr); memptr = nullptr; };
if (hfind) {_findclose(hfind); hfind = 0; };
return retval;
}
Такой подход делает код совместимым, т. к. исключения допустимы не везде. Вместо этого мы используем номера ошибок в POSIX. Механизм исключения используется, чтобы передать управление в хвост.
У меня была идея использовать сигналы. Пишем обработчик сигнала, а в самой функции, что я привёл, пишем raise(SIGINT), я не знаю насколько это правильно.
void handler(int signo)
{
if (signo == SIGINT)
perror("Error: ");
}
int findtext(const wchar_t* file, const wchar_t* text)
{
FILE* hfile = nullptr;
wchar_t* memptr = nullptr;
intptr_t hfind = 0;
_finddata_t data;
_fsize_t& size = data.size;
int retval = 0;
try
{
if ((file == nullptr) && (text == nullptr)) throw (int)EINVAL;
hfind = _findfirst(file, &data);
if (hfind == 0) throw (int)ENFILE;
_findclose(hfind);
hfind = 0;
memptr = (wchar_t*)malloc(size);
if (memptr == nullptr) throw (int)ENOMEM;
hfile = fopen(file, "rt");
if (hfile == nullptr) throw (int)EACCES;
fread(memptr, sizeof(wchar_t), size, hfile);
fclose(hfile);
hfile = nullptr;
retval = _memfind(memptr, text);
}
catch (int err)
{
_set_errno(err);
raise(SIGINT);
};
if (hfile) { fclose(hfile); hfile = nullptr; };
if (memptr) { memset(memptr, '/0', _msize(memptr)); free(memptr); memptr = nullptr; };
if (hfind) {_findclose(hfind); hfind = 0; };
return retval;
}
смысл только в том что exceptions - мы всегда делаем от std и всегда можем взять what(). Почему - ну вот так - хочу.
Ну и комментарию - обработка ошибки работы с файлом - может обрабатываться где-то вообще в др. месте.
Ну и пример кода - был больше про абстракцию, что все ошибки через исключения и отлавливаются они хз где.
if ((file == nullptr) && (text == nullptr)) throw (int)EINVAL;
А тут точно должно быть "И" а не "ИЛИ" ?
Ну, тут традиционный водораздел проходит в том, где находится зона контроля - в момент эксплуатции или в момент передачи в эксплуатацию. Обе стороны имеют аргументы, и де-факто одна сторона никогда не победит.
Это плохой пример.
Исключения позволяют вообще не анализировать код возврата из функции, т.к. оно будет проброшено через все вложенные вызовы сразу до блока try ... catch включая функции не возвращающие значения.
int main(int argc, char const *argv[]) {
try { throw [](){}; }
catch(...) { return 1; }
return 0;
}
Например вы передали в код поток, и внутри работает с ним пытаетесь ловить исключения и всё вроде хорошо, пока вам не передали сетевой поток который умеет кидать другие исключения. И они кидаются глубоко внутри кода.
Отсюда вытекает что не реализация должна обрабатывать исключения, а тот кто её запустил и знает какие сущности ей были переданы.
void code(Context *ctx,Input *input) {
try {
...
} catch(OverrideCase &e) { ... }
catch(...) { ctx->handle(); }
...
inner_function(ctx,...);
...
try {
...
} catch(OverrideCase &e) { ... }
catch(...) { ctx->handle(); }
}
void Context::handle() {
try { throw; }
catch(Case1 &e1) { return; } // ignored case
catch(Case2 &e1) { throw TerminateCase(e1); } // convert exception type
...
catch(CaseN &eN) { ... }
catch(...) { handle_runtime_updated_cases(); }
}
А еще лучше это всё в лямбды завернуть
void code(Context *ctx,Input *inp) {
ctx->crutial("initialize",[=]() {
...
ctx->optional("set hints",[=]() {
...
});
ctx->repeat_on_fail("try to connect",[=](int iteration) {
...
});
...
});
}
Правда выглядит, конечно, не очень.Дело в том, что в рантайме как раз не исполняется код типа if(error). И вверх по стеку ничего не транслируется.
В случае выброса исключения специальный код на низком уровне начинает анализировать стек-фреймы в поисках места куда можно передать управление для обработки исключения. Заодно ещё и удаляются все ресурсы, которые были аллоцированы в пройденых стек-фреймах.
— Пользовательский ввод. Мы пытаемся с ним что-то сделать, и возникает ошибка. Поскольку «можно сделать защиту от дурака, но только от неизобретательного» — ошибки могут быть любые, и исключения тут совершенно к месту. Однако, никакого влияния на производительность не будет, потому что пользователь — по сравнению с компьютером — совершенный тормоз, и исключения по его вине будут происходить от нескольких раз в секунду до одного раза в десятки секунд. Для современного ПК — это на уровне погрешности.
— Некорректные показания датчика. Например, мы извлекаем квадратный корень из расстояния, которое мы получаем от одометра. Внезапно расстояние становится отрицательным. Это, собственно то, на что я спроецировал пример в статье. Тут можно генерировать бесконечное число исключений — но проблема должна быть решена архитектурно. Если датчик несколько раз прислал недостоверные (невозможные) значения — он должен быть отключен. Это — базовый принцип безопасной деградации системы. Работа по недостоверным значения датчика — так себе идея…
— Внешние события (обычно, это сеть). Тут тоже есть где и как накидать себе исключений до потери производительности. Но и это тоже вопрос архитектуры. Если у вас из соединения лезет мусор, который нельзя обработать — нужно либо уведомлять вышележащий уровень (пусть скорость понизит, или еще что), либо рвать соединение через несколько исключений. При этом есть надежда, что всплеск реконнектов увидится на сетевом оборудовании, сгенерируется алерт или хотя бы случится троттлинг (ddos protection). В любом случае, это не та ситуация, когда у нас из всех потоков шарашат исключения, а мы несмотря на это — продолжаем.
Какие выводы? Если накладные расходы связанные с обработкой исключений начинают существенно ограничивать производительность приложения в целом — необходимо переработать архитектуру приложения. В нем явно что-то не так! Иногда архитектуру переработать нельзя по внешним причинам — и тогда просто не нужно использовать в критичных местах механизм исключений (прямые вызовы libc никто не отменял!).
Собственно, Страуструп предупреждал, что механизм исключений не имеет гарантированной производительности…
Ну вот случилось исключение: кончилась память, кончился хард, перестал писаться лог - какая мне разница как при этом просела производительность.
Надо выйти, починить и перезапустить методы, либо завершить работу.
Куда лучше чем альтернативы типа проверять после каждой функции всё ли везде хорошо, при отключенных отключениях (либо вообще грохнуться).
@honyaki или любой другой представитель skillfactory (спонсоров этой статьи), ответьте, пожалуйста, на пару вопросов.
1. У вас есть курс по C++, на котором правда разбираются такие вещи? Этот курс в вашей системе грейдов для какого уровня (junior, middle, senior)?
2. Есть ли аналогичного уровня материалы по Data Science? Если да, то есть ли у вас курсы, в которых об этом рассказывают?
Альтернативное мнение с бенчмарками и совсем другими выводами: Коды ошибок — это гораздо медленнее, чем исключения
Из двух статей можно составить более полную картину :)
Альтернативы исключениям С++ и зачем они нужны