Как стать автором
Обновить

Комментарии 117

int *ptr = malloc(sizeof(int) * N);
if (!p)
{
  // Обработка ошибки выделения памяти
}

Странно, что статический анализатор не выдал ошибку на этот код )))

Андрей. У Вас очень хорошие статьи, читаю с удовольствием. Но вот лично меня терзают сомнения, стоит ли публиковать такое. В том смысле, что если человек этого не понимает, то так писать просто не следует. А стоит пользоваться функциями выделения из (своей или чужой) библиотеки, которая, во-первых, сделаем проверку. Но важнее, что вернет shared. Еще раз - если с умом и этого не требуется, то можно и руками, но редко. Итого имеем выделение и освобождение без проблем. Может у меня параноя, но я все Cuda'вское так же выделяю с кастомными делитерами через shared. Включая даже stream. Просто что бы забыть об их управлении. И это в мега нагруженном Cuda коде. Коментарий для начинающих, естественно.

вернет shared

Какой shared в Си без плюсов?

Это обсуждают. Я видел такие проверки. Значит стоит. :)

Ну как бы общее правило - assert'ом проверяются инварианты, зависящие только от логики кода, а не от внешних факторов.

Другое дело, что корректная обработка возврата 0 из malloc (или нехватки памяти в общем случае) - нетривиальная задача. Как минимум надо гарантировать, что ничего не аллоцируется в коде обработки и разобраться, как при этом взаимодействуют потоки (потому как если память кончилась, она кончилась у всех).

ИМХО assert полезен на стадии отладки, но в релизе нужно делать программу как можно более хрупкой (с умом). Обработка случайного null или not enough memory ошибок это нечто недостижимое, на что можно потратить очень много времени с низким выхлопом. Аргументы функций сюда же, но, возможно, кроме основополагающих API. Программа должна ломаться предсказуемым и безопасным способом, чтобы можно быстро вычислить место ошибки.

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

Хотелось бы посмотреть на правильную проверку выделения памяти в достаточно "среднем" коде где всего-то будет пара тысяч инструкций выделения памяти (не важно - в одном функции или с разнесением по сотням функций (большая часть которых, скорее всего, будут инлайновыми но это не суть). Во что превратится такой код?

А, ну понятно, что опытный программист просто напишет свою глобальную функцию или макрос для выделения памяти и проверки - и будет выделять в одну строчку - но в статье об этом ни слова

опытный программист просто напишет свою глобальную функцию или макрос для выделения памяти и проверки

И что можно сделать в этой общей функции, если проверка не сработала? Разве что exit(MEMORY_EXHAUSTED_CODE)...

Что само по себе не мало.

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

просто временно не принимать новые соединения

Надеясь что память вдруг появится?

Такое может быть в некоторых приложениях.

А почему бы и нет? Кто-нибудь другой упадет из-за нехватки памяти - и вот она, память для нас...

OOM рулетка какая то )

Напоминает эволюцию. Выживут те, кто не выходит.

OOM killer может и до них добраться.

Во-первых, да.

Во-вторых, оно может освободить само то, что брало ранее. Ближайшая аналогия для понимания — “хвост” очереди LRU.

оно может освободить само

Только до этого может захотеть что то ещё аллоцировать.

Как минимум сам сервер может освободить какую-то часть памяти. Для примера можно взять редис, ему может прийти запрос DEL, или у ключа может закончится время жизни, после этого сервер освободит память, используемую для ключа и значения

А во время обработки этого запроса точно никаких аллокаций нет?

В коде конкретно редиса есть, но никто не мешает их убрать. Насколько помню в редиске для аргументов команды зачем-то аллоцируются новые буферы. Я когда писал ради фана свой небольшой клон избавился от аллокаций в процессе разбора комманды (это несложно, ведь все аргументы уже есть в буфере, надо просто доставать их оттуда, не обязательно аллоцировать для этого дополнительную память). Если говорить про сам буфер, в который читается команда, его аллоцировать, скорее всего, будет не нужно, так как в редисе перед запуском уже некоторое количество буферов аллоцируется, после обработки запроса клиента, память под буфер не отдаётся ОС, а возвращается в пул для того, чтобы использовать при обработке нового запроса. Ну и в случае обработки истечения времени жизни ключа аллокаций, по-моему нет.

но никто не мешает их убрать

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

И на самом деле есть аргумент посерьёзнее - OOM killer убивает процесс до того, как malloc возвращает 0, легко проверяется на простом примере. Так что все рассуждения о попытках обработать такую ситуацию - только теория.

например запустить сборщик мусора

И в цикле дёргать malloc пока == NULL? :)

throw... Мы же в C++. Отлов на внешнем уровне вроде как является обязательным с точки зрения здравого смысла.

В С++ практически не используется malloc() подобные функции. Статья явно написана для чисто си, без плюсов.

Мы же в C++

Тогда, если мы пользуемся им на полную, new и так исключение кинет. Но coding guidelines могут запрещать использование исключений, по крайней мере про одну компанию, где так принято, все знают ) Ну и даже с исключениями в большом продукте с кучей сторонних библиотек не так всё просто.

Отлов на внешнем уровне

Так и не понял, что бы это могло значить. Я работал в местах, где исключения использовались... как исключения и вообще никак не отлавливались. Любое исключение должно было приводить к завершению программы и её перезапуску контролирующим демоном.

Что еще интереснее - не до конца ясно, что делать bad_alloc в принципе. Внятного и надёжного способа обработать такое исключение не словив std::terminate просто нет.

Прискорбно. Если все хреново в принципе, тема статьи не в кассу.

О! Что можно сделать? Ну тут многое зависит от конкретного приложения (библиотеки), уровня разработки и опыта самого разработчика, требуемого уровня надёжности и применяемых стандартов выхода из критических ситуаций.

В простых приложениях, безусловно - достаточно просто грамотно упасть (в ггенерацией правильной ошибки, и, если предусмотрено, логированием, в библиотеках ещё и выходить нельзя - там много определяется контрактами этих библиотек - но в простейшем случае это просто должна генерироваться ошибка/исключение "Out of memoery" - а там уже пусть внешний код разбирается - где, в простейшем виде хотя бы где-то в недрах условной точки вхожа функции "main" - всё должно быть огорожено попыткой и последующей обработкой ключевых исключений, приводящих к краху приложения (например, это позволит хотя бы закрыть открытые ресурсы)

В чуть более сложных, особенно многопоточных, приложениях можно подойти к проблеме чуть комплекснее.

Нехватка памяти - это не частое явление под современными ОС (с их файлами подкачки и хитрыми менеджерами памяти; но, например, не всегда включены файлы подкачки (Или кончится место на диске), в ряде случаев могут быть и хуки на процессы, для внешнего контроля за памятью, могут быть и другие случаи; например, 32битные приложения априори не могут обычным путём выделять более 2Гб памяти) - так что задумываться об этом всё-таки нужно. Так что может делать более сложное чем "тривиальное" приложение (кроме как записать логи, выполнить дам памяти, отправить оповещение):

  1. Заблаговременно контролировать нехватку памяти. Конечно, не при каждом выделении, но хотя бы с какой-то периодичностью, и при выделении больших объёмов (в т.ч. неявных, т.е. выполняемых косвенно "чужим" кодом. Причём - чем свободной памяти становится меньше - тем чаще контролировать. Сами критерии, само собой, индивидуальны.

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

  3. При нехватке памяти приложение должно сократить объёмы кеширования. И вообще очистить все имеющиеся кеш-данных (ну кроме самых критических, если такие выделены оидельно)

  4. Приложение может иметь специальную секцию кода и данных для проведения ревизии выделенной памяти - чтобы оценить объёмы утечек - если их много - то имеет смысл перезапуститься.

  5. Так же, такая ревизия может просто выгрузить какие-то объёмные данные - к которым давно не было обращений (само собой - такая возможность заранее должна быть предусмотрена).

  6. Серьёзное приложение должно либо иметь свой менеджер памяти, либо запускаться в нескольких процессах - с возможностью относительно безболезненно гасить некоторые процессы (выгружая ценные данные их них) - и перезапуская процесс!

  7. При невозможности плавного рестарта по частям - приложение должно перейти в стадию безопасного режима с ограниченной функциональностью - чтобы пользователи (или автоматические алгоритмы) смогли выполнить операции для сохранения рабочих данных перед крахом приложения. При этом, по возможности, надо освободить память от всех остальных объектов.

  8. При выполнении каких-либо критических операций так же можно заранее создавать специальный буфер выделенной памяти - и, если памяти вдруг будет не хватать:

    8.1 Если есть свой менеджер памяти - то выделять память можно в этом буфере

    8.2. Можно освободить этот резервный буфер и, если возможно, послать менеджеру памяти команду на реструктуризацию памяти

    8.3. Можно заранее создать и целый процесс, который выделит большой объём резервной память - а потом просто прибить его - системный менеджер памяти сразу начнёт реструктуризацию памяти

Ну это на вскидку я набросал. Системные программисты наверняка бы предложили ещё что-то.

Да, я говорю "системные программисты" - потому что такого рода задачи - это явно уже не прикладной уровень! Поэтому по хорошему всё управление памяти в серьёзных приложениях нужно выносить на уровень специализированных библиотек! Так же как и управление обработкой исключений (особенно не прикладных).

А вообще - моё мнение такое - что это вообще не задачи программистов конечного ПО, включая рядовые библиотеки! В современном мире программной разработки этим должна заниматься платформа, на которой ведётся разработка, вместе с компилятором/оптимизатором - хотя бы на уровне разработки прикладного ПО. То есть, прикладной программист никогда не должен напрямую вызывать функцию а-ля "malloc" - он должен выделять память через "управляемый" API - который уже сам позаботится о корректной обработке выделения памяти. И через "тот же" API программист заранее должен настроить все, важные, ему нюансы обработки проблемных ситуаций - как на них реагировать. А этот "управляемый" API будет уже обеспечивать выполнение вышеописанных пунктов, при незначительном снижении производительности - а где она особенно критично - так писать код в особых обрамлённых секциях - где лишние проверки будут отключены, но память будет более жёстко контролироваться заранее - до входа в такую секцию - и если оценка даст высокий риск проблем - то попытаться их решить до входа в секцию и при невозможности - не входить в неё - осуществив "отказ в сервисе", и выполнив переход какое-то другое кризисное состояние.

P.S.

Само собой, все пункты выше должны быть реализованы с особо тщательным контролем своей работы и выделения памяти! Как минимум постараться использовать уже созданные заранее объекты, сначала постараться легко очистить доступную для этого память, а потом уже выделять новую под свою работу. Код тут может больше стараться выделять память на стеке, чем в куче. Но и на стеке память, тоже кончается (и порой даже чаще, чем в куче) - но это уже другая тема - и тут свои способы решения чтобы жёстко не упасть!

Все пункты нацелены на то, чтобы не допустить возврата 0 из malloc ) Это всё хорошо и правильно, но конкретно к проверке результата работы malloc отношения не имеет.

 к проверке результата работы malloc отношения не имеет.

Да ладно! Почти все пункты связаны с обработкой ситуации, когда malloc вернул 0

Но да да - это всё скорее к статье, которая не была опубликована на Habr - но тут как раз началось обсуждение общей проблемы - а не того, что не стоит проверять результат как через "assert". Но мой ответ всё-равно по теме - он обосновывает почему так не стоит делать - и что можно было бы сделать вместо проверки через "assert" (применение которой лишает таких возможностей). В целом - это скорее задел на возможную статью какому-либо автору (но это не мой профиль, так что от меня пока не ждите):

  1. было: Почему на провеять результат malloc

  2. было: Почему не годится проверка через "assert" и как правильно проверять

  3. возможно: Что делать когда проверка показала, что malloc вернул нулевой указатель

ИМХО, правильно было вообще это всё публиковать одной содержательной статьи! Но у автора совсем другой кругозор - поэтому его статьи о несколько более узкие!

Да ладно! Почти все пункты связаны с обработкой ситуации, когда malloc вернул 0

Какой конкретно пункт говорит о том, что делать, если стандартный malloc вернул 0? Разве что пункт 7, но реально, если нельзя выделить даже несколько байт, сложно обеспечить хоть какую то функциональность.

3, 4, 5, 6, 7, 8 - все эти пункты можно применять, когда  malloc вернул 0, но аккуратно! Но, лучше не пренебрегать и пунктами 1, 2 - тогда остальные пункты можно применять заранее! И это куда безопаснее, чем сразу столкнуться с тем, что  malloc вернул 0

если нельзя выделить даже несколько байт, 

  1. Для освобождения памяти выделять её не надо. Тут имеет смысл в том, чтобы заранее выделить память под объекты, с которые потребуется работать в такой ситуации.

  2. Имеет прозапас выделенный резервный блок памяти, например в 1Мб, вы сможете обезопасить себя в ситуации необходимости получить небольшую память при её критической нехватке: тут даже освобождать ничего не надо (или иметь свой продвинутый менеджер памяти) - просто возьмите нужную память с конца такого блока и уменьшите значение переменной его размера. Но это, конечно, в простейшем виде, лишь решение в кризисной ситуации - чтобы не падать, и хоть как-то продолжить работу и выделять память (пока кризис не будет преодолён или приложение не завершит аккуратно свою работу)

  3. Ситуация когда malloc вернёт 0 при выделении нескольких байт - крайне редкая (асимптотически стремящаяся к нулю) - и скорее это уже ошибка архитектуры приложения. Обычно такой командой всё-таки выделяют куда большего объёмы блоки памяти, ну или хотя бы вероятность того, что память кончится на выделении блока длиной в хотя бы в несколько килобайт куда выше, чем длиной в несколько байт. Поэтому и есть пункты 1, 2 - которые имеет смысл выполнять при выделении больших блоков памяти, и заранее готовиться к её критической нехватке

3, 4, 5, 6, 7, 8 - все эти пункты можно применять, когда  malloc вернул 0, но аккуратно!

Очень аккуратно и сложно (если приложение большое, пишется разными командами) - надо гарантировать, что мы дойдём от проблемной точки до кода, который может что то освободить, ничего не аллоцируя по пути.

Но, лучше не пренебрегать и пунктами 1, 2 - тогда остальные пункты можно применять заранее! И это куда безопаснее, чем сразу столкнуться с тем, что  malloc вернул 0

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

Обычно такой командой всё-таки выделяют куда большего объёмы блоки памяти

Если у нас связный список (или дерево, или граф общего вида) с несколькими байтами полезной информации в узле, но большим количеством узлов - очень легко съесть всю память маленькими аллокациями.

Очень аккуратно и сложно (если приложение большое, пишется разными командами) - надо гарантировать, что мы дойдём от проблемной точки до кода, который может что то освободить, ничего не аллоцируя по пути.

Ну почему же так строго.

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

Во-вторых, алгоритм реанимации так должен быть составлен, что в первых же своих командах сможет быстро освободить хоть какую-то "лишне" занятую память - это не сложно архитектурно предусмотреть - все ссылки на нужные объекты должны быть заранее подготовлены. Да и про выделение резервного блока памяти я написал.

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

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

В-пятых, как я уже сказал, обработка данной ситуации это исключительный случай - тут либо надо сразу переходить в блок кода, занимающийся его обработкой. Либо генерировать специфическое исключение - разматывать стек (где, само собой корректно должны быть написаны обработчики исключений, освобождающие лишние ресурсы), и, доядя дишь, до нужной точки - проводить уже обработку исключения по критическому сценарию - а там уже далее принимать решение - мягко завершаться, перезапускаться, сохранять или нет как-то контекст выполнения/пользовательские данные (а диск), и можно-ли как-то возобновить текущую работу без жёсткого перезапуска/завершения.

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

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

В-восьмых, как я написал выше - всё-таки я считаю, что этим, в большинстве случаев, должен заниматься не программист, а платформа и компилятор!

Если у нас связный список (или дерево, или граф общего вида) с несколькими байтами полезной информации в узле, но большим количеством узлов - очень легко съесть всю память маленькими аллокациями.

Так не делают, во всех коллекциях стараются аллоцировать большими порциями. И потом, потихоньку использовать (причём чем больше занято - тем больше следующая порция). Иная архитектура - скорее всего грубая ошибка (ну или студенческий код). Как минимум так делают из-за производительности - и частая аллокация, и частые разрозненные переходы от блока к блоку памяти - могу заметно снижать производительность!

32битные приложения априори не могут обычным путём выделять более 2Гб памяти

Код на C:

#include <stdlib.h>
#include <stdio.h>

#define NELEMENTS(a) (sizeof(a) / sizeof*(a))

#define SIZE_1G ((size_t)1024 * 1024 * 1024)

int main(void) {
	void *const a[] = {
		malloc(SIZE_1G),
		malloc(SIZE_1G),
		malloc(SIZE_1G),
		malloc(SIZE_1G)
	};

	for (size_t n = 0; n < NELEMENTS(a); ++n) {
		printf("ptr: %p\n", a[n]);
		free(a[n]);
	}

	return EXIT_SUCCESS;
}

Компиляция:

$ gcc -Wall -Wextra -pedantic -m32 -O3 -o 32 1.c 
$ 

Запуск:

$ ./32
ptr: 0xb7bff010
ptr: 0x77bfe010
ptr: 0x1664c010
ptr: (nil)
$ 

Я не знаю, как мне удалось нарушить ваше "априори" в 2ГБ, выделив 3ГБ.

2Gb было стандартом в случае 32битных приложений на 32битных ОС; под 64битной ОС код ядра в адресное пространства не мапится, доступны почти все 4Gb

Странно - у меня на 64битной ОС 32битные приложения падают, при попытке выделить более 2Гб на процесс - но это не мои приложения - сам уже очень давно этим экспериментально не интересовался - но заинтриговали - я попробую провести эксперимент

Если что я говорил, только про windows - под*nix не проверял (но там нет эмуляции 32битной среды в 64битной ОС, но как там выделяется память в 32битной среде не знаю)

Потому что многие компиляторы не добавляют флаг (PE заголовок) процесса (IMAGE_FILE_LARGE_ADDRESS_AWARE), который позволяет выделять 32 битным приложениям более 2гб

Вот оно в чём дело - вот теперь понятно

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

Наряду с обычными рабочими потоками заранее создаём поток восстановления – который в обычном режиме может периодически проверять общий объём свободной памяти и на основе этого значения  применять какие-то превентивные меры. Но обсуждать далее буду только уже состояние кризиса. В этом случае код, попытавшийся выделить память, и столкнувшийся с её нехваткой (не буду тут акцентировать момент на то, каким образом это зафиксировано, например, это функция malloc вернула нулевой указатель) выставляет общий флаг (условно) для выставления пометки  кризисного режима нехватки памяти. Вот его и отслеживает данный поток – после чего начинает процесс восстановления доступности памяти. Но тут есть ряд важных деталей, о которых ниже.

Во-первых. Предлагаю сразу разделить все операции выделения памяти на несколько приоритетов, задаваемых явно при выделении памяти:

1.       Пофиг – Алгоритму пофиг на данное выделение памяти, он либо напрямую его не использует, либо готов к тому память не будет выделена и это не повлияет существенным образом на его работу. Например, это может быть выделение под кэш, или какие-то вспомогательные данные, не влияющие на основную работу

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

3.       Важно – Алгоритму нужно это выделение памяти, но он готов к проблемной ситуации – либо как-то обработает, либо, «условно упадёт» - перейдя в режим восстановления работы

4.       Критично – Алгоритму критически нужно данное выделение памяти, он «до последнего» будет надеяться, что память ему всё-таки дадут

5.       Обязательно – Алгоритм остро нуждается в выделении памяти – любой отказ смерти подобен – нужно изыскать её по что бы то ни стало – жертвую всем чем можно (вообще, в обычной работе не должно быть таких запросов на выделение памяти)

6.       Резервное – Алгоритм запрашивает резервную память – это уже из области режима восстановления (про резервную память писал ранее – память, которая отдельно заранее зарезервирована)

7.       Восстановление – Этот запрос на выделение памяти идёт из алгоритма восстановления –выделяется в отдельный приоритет, чтобы не уйти в рекурсию

8.       Системно – Прочие системные выделения памяти с наивысшим приоритетом – невозможность выделения чревата крахом всей системы

Первые три приоритета при нехватке памяти просто не будут её получать. Но третий режим готов немного подождать, включив глобальный маркер.

Четвёртый приоритет будет ждать пока не будет выставлен другой глобальный маркер – информирующий о том, что с выделением памяти действительно большая проблема и простыми путями её не получить – грубо говоря, без жертв не обойдётся. Это значит, что и резервная память уже исчерпана. Текущий алгоритм скорее всего будет «падать» (в исключение или как-то иначе прерывать свою работу).

Пятый приоритет – тоже самое, что и четвёртый – но выставляет маркер требования жертв – тут могут быть завершены другие потоки (НЕ НАХОДЯЩИЕСЯ В КРИТИЧЕСКИХ СЕКЦИЯХ) – коли памяти так и не будет.

Шестой, седьмой – это специальный режим выделения памяти из алгоритмов восстановления памяти. Не начинают других процессов и нечего не ожидают.

Восьмой приоритет – то же самое, что и четвёртый – но требует немедленного выделения памяти – если никаких резервов нет – то он алгоритм «упадёт», и скорее всего потянет за собой весь процесс.

Соответственно далее обработка выделения памяти может вести себя несколько по-разному – для разных приоритетов выделения памяти. Но главное – она будет выставлять разные глобальные маркеры критичности ситуации.

 

Во-вторых. У нас есть отдельный поток восстановления. Он анализирует выставленные маркеры и принимает различные меры восстановления:

1.       Чистит кэш

2.       Сокращает буферы

3.       Удаляет временные и вспомогательные объекты (которые были таковыми заранее зарегистрированы)

4.       Выгружает какие-то данные, условно, на диск

5.       Выполняет какую-то упаковку больших данных

6.       Насильно (или поначалу мягко) завершает выполнение каких-то процессов (потоков)

Всё это делается очень аккуратно, с некоторой расстановкой приоритетов, с постоянным контролем свободной памяти.

В-третьих. Все алгоритмы восстановления стараются не выделять память на куче – но если нужно – то делать это с контролем и в режимах: Резервно, Восстановление, Системно – но чаще именно в режиме Восстановление, т.е. остальные режимы слишком специфические

В-четвёртых. Когда памяти становится достаточно  - то делается некоторая задержка, в потоке восстановления, чтобы стартовали все приостановленные потоки, после чего память снова проверяется – и если её по-прежнему достаточно – снимаются кризисные маркеры и поток временно засыпает до следующей автопроверки.

В-пятых. Если восстановление так и выходит на необходимый объём памяти – выставляется маркёр отказа в обслуживании – все запросы памяти получают отказ – и алгоритмы, как и потоки сворачиваются – приложение устремляется к некоторой базисной точке – происходит глобальная чистка памяти – далее приложение перезапускает свою работу – но если памяти по-прежнему не хватает – приложение окончательно «падает»

 

Всё это выглядит красиво - и возможно сработает, но только если приложение пишется с нуля небольшой командой опытных специалистов, разделяющих общие взгляды на такую архитектуру. В реальной жизни, когда у нас смесь legacy и third party, внедрить что то такое нереально.

Да при "чистой" разработке слишком многое надо предусмотреть. Каждый раз при аллокации надо маркировать данные, обрабатывать ситуации, когда при попытке освободить неважные данные (кеш/буферы) один из потоков как раз с ними и работает или указатели/ссылки на эти данные лежат в куче разных мест...

Правильный заголовок для статьи должен быть примерно таким:

Почему проверять результат вызова c помощью assert плохая идея

assert - это инструмент для описания контракта, а не инструмент для проверки чего бы там ни было в рантайме)

Во многих проектах пишут макрос XMALLOC где по аналогии с assert происходит проверка, но она работает всегда в любом варианте билда.

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

Нет, упасть — это очень плохая идея. Сервер не должен падать под нагрузкой.

Для этого надо строить свою систему распределения памяти так, чтобы всегда оставался запас для системы, а не доводить ситуацию до полного исчерпания.

Что толку от "работающего" сервера, который ничего не может сделать полезного?)

Сервер как минимум продолжает обслуживать текущих клиентов. Это полезно.

Если память он выделить не может, никого он не обслужит)

Не может для новых клиентов, а для старых память уже выделена, и клиенты работают.

Я понимаю, что существуют достаточно продуманные системы, которые требуют фиксированного, заранее известного количества ОЗУ для работы, и память можно выделять один раз, заранее, ничего больше не потребляя, но в подавляющем большинстве случаев это не так.

Выделение памяти разного размера бывает. Например, под буфер 500 мегабайт памяти может не быть, а под объект в 10 килобайт будет.

Или сейчас памяти недостаточно, а потом один из клиентов отключился, память освободилась — и теперь памяти стало хватать.

Неправильно думать что однажды получив ошибку выделения памяти мы будем получать её всегда. Не хватает памяти — это преходящее явление.

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

Это же рай для злоумышленника. Из UserMode можно уничтожить систему.

После этого предложения, у меня возникло глубокое подозрение, что вы не совсем понимаете предмет нашего обсуждения.

Если я всё же неправ, распишите более подробно вектор атаки и причем тут user mode в частности.

int *ptr = malloc(sizeof(int) * N);
if (!ptr) ...
Для N=0 имеем "the behavior of malloc is implementation-defined", получается классической проверки всё равно не достаточно, могут быть ложные срабатывания.
Что если беззнаковое выражение sizeof(int) * N переполнится и выделится меньше, чем ожидалось. Сплошные вопросы от кода C...

Основная претензия и к этой статье и к "Четыре причины ..." - смешивание NULL и 0. Да, согласен, обычно это так. Однако, например, в стандартах не говорится про ноль. Там говорится про NULL или null pointer.

Придерживайтесь стандартов, тем более разработчику анализатора кода соответствие стандарту - это в принципе обязательно. Тогда и проверка

if (ptr == NULL)

будет правильным, единственным и очевидным выбором.

Прим.: почему на это указываю? В недавней статье https://habr.com/ru/companies/ruvds/articles/788654/ было обсуждение, что как-бы существуют (или существовали) платформы (и компиляторы под них), где NULL не равно 0. Упс!

NULL всегда равно 0, о есть нулевому указателю. Просто этот указатель не обязан указывать на нулевой адрес.

Однако, например, в стандартах не говорится про ноль.

Стандарт языка Си ISO/IEC 9899:2011 § 6.3.2.3 Pointers :

Hidden text

An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

В K&R § 5.4. Адресная арифметика:

Hidden text

Язык С гарантирует, что 0 никогда не бывает адресом данных, поэтому возвращение нуля можно использовать как сигнал аварийного завершения — в данном случае нехватки места в буфере памяти.

Указатели и целые числа не употребляются вместе как взаимозаменяемые значения. Нуль — единственное исключение. Нулевую константу можно присваивать указателю, и указатель можно сравнивать с простым числовым нулем. Часто вместо нуля используется символическая константа NULL, которая более четко показывает, что это не просто число, а специальное значение указателя.

Насчет стандарта C++ не знаю.

Тогда и проверка

if (ptr == NULL)

будет правильным, единственным и очевидным выбором.

Функция malloc возвращает null pointer (ISO/IEC 9899:2011 § 7.22.3.4).

NULL - это просто макрос (символическая константа) нулевого указателя, которая действительно определяется реализацией (ISO/IEC 9899:2011 § 7.19 3). Т.е. он относится к стандартной библиотеке (определен в заголовочном файле), а намертво не вшит в язык, как ключевые слова, например.

Таким образом, будут легальны и остальные проверки:

if (!ptr)

if (ptr == 0)

как-бы существуют (или существовали) платформы (и компиляторы под них), где NULL не равно 0.

Если заниматься буквоедством, то целочисленный литерал 0, приведенный к указателю на void, это не ноль :)

#define NULL ((void *)0)

"Нет памяти" нормально например в рендер системе игровых движков, там памяти всегда в притык (особенно на консолях). Для таких случаев запускаются разные алгоритмы очистки кешей моделей или мешей, да неприятно, гдето недобалансили сцену, но это не повод падать. Ну и не только в рендере, физика тоже может набирать активные сущности, пока есть память в пуле, а как закончилась переходить на резервный с упрощённой схемой. Так сделано например в игре AC: Unity, активная область симуляции нпс вокруг игрока меняется от общей загрузки системы, все что не лезет в основной пул стимулируется упрощённо в дополнительном

"Нет памяти" нормально например в рендер системе игровых движков

Вопрос только, как оно определяется - именно по 0 из malloc (что сомнительно) или явным отслеживанием свободной памяти и каких-то действиях, когда её становится мало (но не кончилась полностью). По крайней мере под Linux получить 0 из malloc просто нельзя, как уже написал в другом комментарии.

угу, не совсем правильно написал, врядли мы доберемся до малока в чистом виде в движках, обычно всю доступную память движок забирает на старте у системы. Но это это не отменяет того, что 0 там может прийти из перегруженного малока, поэтому правила теже самые. Ну и на консолях (freebsd на плойке) нет виртуальной памяти в привычном смысле, т.е. получить 0 из малока вполне реально

поэтому правила теже самые

Нет, потому что 0 из "перегруженного маллока" вполне может придти и система при этом будет в рабочем состоянии.

нет виртуальной памяти в привычном смысле

В любом случае если там 64битное адресное пространство, "0 из malloc" означает что кончилась память в системе в целом, что не очень хорошо для работы ядра и критически важных процессов, поэтому OOM killer и прибивает процесс, пока не стало совсем плохо.

А если адресное пространство 32-битного процесса кончится?

32 бита на 64битном процессоре сейчас скорее редкость ) Да, в таком случае исчерпание адресного пространства (при достаточном количестве оперативки) для системы не критично, возможно и дойдёт до возврата 0 из malloc (если glibc где то по дороге не упадёт).

Это прям специфическая задача. Офисному приложению или просмоторщику картинок, вообще не нужонны такие сложные схемы.

Конечно мелкому софту сильные заморочки не нужны - я выше в своём комментарии об этом написал. Но тут дело в том, что даже простое приложение без какой-либо проверки не упав сразу может нанести вред другим компонентам и приложениям ОС изменив их память! Поэтому даже у простого приложения хотя бы примитивная обработка должна быть - чтобы хотя бы просто сразу упасть - всё-таки нужна. И данная статья говорит, что проверка через "assert" даже упасть может не помочь!

Вроде как эта проблема только Си и C++ (и каких-то вариаций)

Вот управляемые приложения будут генерировать исключение "Out of memory" при всех попытках выделения памяти при её недостатке (ну у них просто нет такой низкоуровневой операции, интересно что там по факту в инструкциях машинного кода генерируется), как, скажем, и Delphi - и наверное много других более современных нативных ЯП.

Что, в очередной раз, говорит о том - что Си и С++ - это ЯП "далеко не для всех и вся" - а только для особо "повёрнутых гиков" - которые знают что делать с "гранатой без чеки, находясь внутри синхрофазотрона" при решении задачи "как безопасно устроить кольцевой салют, искрящийся со скоростью точно и строго: одна искра в 37 наносекунд - вопрос жизни и смерти"!

Интересно - а как себя поведёт тот же C++ на операторе new? - ведь в статье речь только о проблемах низкоуровневого выделении блока памяти!

Но тут дело в том, что даже простое приложение без какой-либо проверки
не упав сразу может нанести вред другим компонентам и приложениям ОС
изменив их память!

Так как сейчас в большинстве случаев память виртуальная приложения не имеют доступа к памяти друг друга и тем более к памяти, используемой ос

Ну тогда автор статьи (той что не на хабре) не прав - меня то за что минусовать

А с чего вы взяли, что именно я вам минус поставил? Ну а так за то, что ложную информацию распространяете

Причём здесь это. Записывая по случайному адресу можно испортить данные самого приложения.

Я оспорил факт, что можно попортить данные других приложений или даже ОС. То, что можно испортить данные приложения, в котором нет проверки на NULL (при условии, что его не убьёт oomkiller) я не оспаривал

Приложение не может получить доступ к чужой памяти, ни специально, ни случайно. Если, конечно, для этого специально не выданы права процессу (WriteProcessMemory). Так что, если у приложения косяк с указателями или в целом с работой с памятью, то он испортит её только себе.

Да и собственно, при создании 64 битного приложения, даже если "настоящая" оперативная память закончилась, ОС (Винда, например) всё равно позволит выделать память, свапнув в кеш чью-то выделенную память.

Помнится мне, я писал приложение для подсчета больших чисел столбиком. Т.е. просто хранил число строкой и считал, как считают в школе. У меня тогда было где-то 8гб ОЗУ, но приложение съело все 16гб, а потом я перезапустил комп, т.к. он перестал реагировать =)

если у приложения косяк с указателями или в целом с работой с памятью, то он испортит её только себе.

Из-за испорченной у себя памяти оно вполне может покорёжить чужие файлы.

Но тут дело в том, что даже простое приложение без какой-либо проверки не упав сразу может нанести вред другим компонентам и приложениям ОС изменив их память!

Не верно со времён i386 и ОС на нём.

Читать про защищённый режим работы CPU, страничную организацию памяти и защиту памяти.

В частности

Решаемые задачи

  • поддержка изоляции процессов и защиты памяти путём создания своего собственного виртуального адресного пространства для каждого процесса

и далее по ссылкам.

В любом случае можно испортить свою память. Это может иметь весьма печальные последствия.

С этим-то как раз все согласны, в цитате сказано, что можно испортить память другим приложениям и самой ОС. Потенциально - да, действительно можно, через уязвимости самой ОС, но это уже целенаправленное действие, а не случайность из-за обращения по невалидному адресу.

Неправильным решением будет использовать для этого макрос assert.

Неправильным решением будет использовать макрос assert в принципе для чего бы то ни было, кроме несерьёзных случаев, типа "побаловаться".
Потому что в Release'е проверки не будет, но визуально создаётся иллюзия её наличия.

Мне одному кажется, что язык программирования, требующий таких танцев с бубном, морально и физически устарел?

А как себя ведут себя при полном исчерпании памяти, скажем, Go или Rust?

Вроде как все современные ЯП исключение генерируют - ну а дальше как обработаешь!

вроде даже C++ командой new - будет ошибку генерировать. То есть проблема только в низкоуровневом malloc (и подобным) - прямого доступа к которому в современных, даже нативных, ЯП нет.

Если я не ошибаюсь (поправьте, коли не так)!

Старый Паскаль или Бейсик, или Фортран, вот изначально не имели такого доступа - и всегда генерировали исключение (но не всегда его можно было обработать - это уже прерогатива относительно современных реализаций).

Все управляемые и интерпретируемые ЯП жёстко контролируют работу с памятью и генерируют исключение!

Ну уже как обрабатывать исключение - это дело второе - сразу по месту возникновения (в т.ч. в своей фабрике создания объектов и выделения памяти) или где-то выше по стеку. Главное - что код, где возникла проблема, никак сам по себе продолжить своё выполнение не сможет! Ну только если ошибка явно будет обработана и далее явно будет разрешено выполнение - поэтому нельзя писать обработчики ошибок, обобщённые сразу для всех ошибок (если они не будут передавать ошибку далее по стеку)

Ну уже как обрабатывать исключение - это дело второе

На мой взгляд это как раз интересное (если говорить про исчерпание памяти). Проверять код возврата malloc() или ловить исключение - это уже не так существенно.

Проверять код возврата malloc() или ловить исключение - это уже не так существенно.

Вы не правы. Тут принципиальная разница:

  1. Если Вы не будете проверять код возврата malloc - ваше приложение может вести себя крайне не опасно в критической ситуации. Причём проверять нужно сразу - в каждом месте использования оператора (либо писать макрос - и использовать везде) - легко где-то пропустить! Да и боллерплейт кода много писать нужно.

  2. Если не проверять исключение - то ваше приложение в худшем случае попросту упадёт. И проверять исключение тут можно в любой точке раскручиваемого стека - даже в нескольких - легко можно контролировать все ключевые места проверки и обработки. Да и исключение позволяет легко выйти из более глубокого кода - в какое-то более понятное место, где и произвести обработку. Попутно ещё и ресурсы высвобождая (в файнал секциях - которые надо предусмотреть, при работе в модели исключений; ну у Go - там своя кухня обработки ошибок - скорее близкая к п.1. - она мне не нравится; но при невозможности выделения памяти Go будет создавать "исключение" - панику - её можно обработать "особым образом")

Если Вы не будете проверять код возврата malloc

Напомнят на code review ) Да, исключения усложняют возможность проигнорировать ошибку - но насколько корректной будет обработка в в десятке-другом уровней выше по стеку, сказать сложно (например, какой нибудь деструктор или секция finally начнёт что-то писать в логи, попытавшись при этом выделить ещё несколько байтиков...)

но насколько корректной будет обработка в в десятке-другом уровней выше по стеку

Она либо будет - что уже хорошо. Либо приложение завершится - что не самое плохое - если ничего не будет

 (например, какой нибудь деструктор или секция finally начнёт что-то писать в логи, попытавшись при этом выделить ещё несколько байтиков...)

Я выше много написал о том, что такая ситуация почти невероятна - если об этом вообще задумываться - т.е. целенаправленно обрабатывать исключение "Out of memory" не будучи при этом полным дилетантом! А полным дилетантам обрабатывать такие ошибки самостоятельно не стоит в принципе (как и допускать их к таким проектам - где это важно)

Да и в чём тут преимущество - если обрабатывать не исключение, а возврат malloc? Обработку которого пропустить куда проще (ну если нет автоматизации этого процесса)

А ещё - исключения могут возникать в стороннем коде и в библиотеках (что правильно для них) - так что их так или иначе надо обрабатывать!

Да и в чём тут преимущество - если обрабатывать не исключение, а возврат malloc?

Да никакого преимущества. Моя точка зрения: единственный корректный способ обработки нехватки памяти от системного аллокатора - не допускать такой ситуации. Пользоваться своими пулами фиксированного размера, из которых можно при необходимости выкидывать некритичные для логики работы данные (кеш и т.п.).

 если об этом вообще задумываться - т.е. целенаправленно обрабатывать исключение "Out of memory" не будучи при этом полным дилетантом!

Деструктор или секция finally не знают, по какой причине они вызваны - там может быть просто код, который по логике надо выполнить всегда до выхода из блока, автор и ревьюеры могут не подумать о всех возможных исключениях, которые могут туда прилететь.

Деструктор или секция finally не знают, по какой причине они вызваны - там может быть просто код, который по логике надо выполнить всегда до выхода из блока, автор и ревьюеры могут не подумать о всех возможных исключениях, которые могут туда прилететь.

Вот это правильное замечание - но опять же - это критично если память уже совсем кончилась - а это, как я написал, крайне мало вероятно. Но да - к файнал блокам и деструкторам нужно относится внимательно (и об этом в часто пишут в доках к ЯП) - чтобы много памяти не пытались выделять. В идеале - я уже написал выше - надо как можно раньше выставлять маркер режима работы с нехваткой памяти (как минимум две стадии: просто когда памяти крайне мало, и когда её уже критически мало) - и учитывать его в файнал блоках. Стараясь передавать обработку в особую единю секцию кода - специально созданную для такой ситуации, работающую в основном либо с уже созданными ранее объектами, либо размещая их на стеке; либо управляя отдельным выделенным резервным буфером памяти - задача которого аккуратно освобождать переданные ему ресурсы; а в первую очередь кэш разного уровня глобальности и приоритетности. И всё под контролем текущего объёма свободной памяти. То есть, в таких проектах нужно сразу вводить такой маркер и проверять его в особо ответственных местах местах, а не разбираться в исключениях

Короче - как уже выше сказал - что делать в ситуации когда Out of memory - это всё тянет на целую весьма серьёзную статью! У тут у нас просто бла-бла-бла... кстати, что там Страуструп - ничего не писал на эту тему (я его не читал, к сожалению, C++ не мой любимый ЯП, скорее наоборот; хотя истоки проблемы как раз тянутся от языка Си - но у, прочитанного мной Подбельского, вроде ничего на эту тему не было)?

Вроде как все современные ЯП исключение генерируют - ну а дальше как обработаешь!

И чем это принципиально отличается от провери врезультата malloc() по месту вызова?

Более того, исключение полностью ломает стек вызова, а значит логику восстановления (roll out) очень сложно реализовать - поэтому во многих сферах исключения запрещены к использованию.

Ну и можно же попросить new не бросать исключения, тогда результат нужно будет проверять точно так же как и в случае с malloc() и это, между прочим, стандартная практика в embedded и подобных системах.

Старый Паскаль или Бейсик, или Фортран, вот изначально не имели такого доступа - и всегда генерировали исключение

Про Бейсик и Фортран не помню, а пот про Pascal слышать забавно:

getmem - Allocate new memory on the heap (аналог malloc)

new - Dynamically allocate memory for variable

Главное - что код, где возникла проблема, никак сам по себе продолжить своё выполнение не сможет! Ну только если ошибка явно будет обработана и далее явно будет разрешено выполнение 

Ну вот же, сперва отрицание, а потом правильный ответ :)

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

А кто-то про обобщённый говорил?

И чем это принципиально отличается от провери врезультата malloc() по месту вызова?

Смотрите выше уже отвечал

Более того, исключение полностью ломает стек вызова, а значит логику восстановления (roll out) очень сложно реализовать - поэтому во многих сферах исключения запрещены к использованию.

Я, честно, не в курсе этой проблемы - мне тут нечего сказать. Но от исключений всегда можно легко перейти к линейной обработке - блоком обработки этих исключений - главное понимать что Вы готовы обработать в данном кадре стека! А файнал блоки доделают остальную работу - если будет исключение, которые Вы тут обрабатывать не готовы.

Ну и можно же попросить new не бросать исключения, тогда результат нужно будет проверять точно так же как и в случае с malloc() и это, между прочим, стандартная практика в embedded и подобных системах.

Я не спец по C++ - но согласен так можно - но это Вы уже специально и явно это делаете в своём коде - и несёте за это ответственность! Ну и на сторонний код - это может как повлиять, так и не повлиять - это как в C++ уж напишете - там свободы много - как и вариантов последствий с высокой сложностью поиска непонятных плавающих ошибок и неявного, трудно выявляемого поведения!

Поймите, я ни в коей мере не пытаюсь никого убедить, что исключения - это панацея во всех ситуациях! Но с ними - всё-таки проще, а главное надёжнее выкрутиться в подобной ситуации (и от них всегда можно перейти к модели без распространения исключений, в C++ это проще простого сделать, как вот выше Вы и приводите один из примеров). Если программист крут и разработка очень ответственно ведётся - то можно и без исключений! Я не против! Суть то не в этом. Я то просто ответил на то, что делать когда память закончилась - именно на такой пост я дал свой изначальный ответ! И я ставил исключения как самое важное что тут может быть!

Но, всё же - для относительно простых проектов - исключения (и какая-то их обработка выше по стеку) - самое лучшее решение!

getmem - Allocate new memory on the heap (аналог malloc)

new - Dynamically allocate memory for variable

Ну это, кажется уже современные хуки. Внешний переключатель поведения функции. Ранее только исключение было, кажется

И в описании функции getmem ошибка - что исключений эта функция не создаёт - как раз-таки может создавать - судя из описания (в описании процедуры new всё ок)!

Ну вот же, сперва отрицание, а потом правильный ответ :)

Я Вас не понял - что Вы имели в виду?

Я, честно, не в курсе этой проблемы

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

Наверное интересно увидеть на дисплее автомобиля, мчащегося под 300 км/ч по немецкому автобану - прошивка ЭБУ выполнила недопустимую операцию и контроллер будет выключен? :)

это Вы уже специально и явно это делаете в своём коде - и несёте за это ответственность!

А разве при написании программ бывает иначе?

что делать когда память закончилась - именно на такой пост я дал свой изначальный ответ! И я ставил исключения как самое важное что тут может быть!

В некоторых ситуациях объект исключения тоже выделяет память в момент вызова, поэтому бросить std::bad_alloc не получится - приложение перейдёт в бесконечный цикл аллокации исключения и упадёт с stack overflow.

Если программист крут и разработка очень ответственно ведётся - то можно и без исключений!

Почитайте об embedded разработке, в частности - bare-metal, полезно будет, там нет операционной системы, там нет глобального пула памяти, там невозможны исключения в принципе.

Это не крутость или простота - это одна из областей где С и С++ находят большое применение сейчас.

Ну это, кажется уже современные хуки. Внешний переключатель поведения функции. Ранее только исключение было, кажется

Nope

Специально нашёл какой-то древний сайт на народе (оказывается он ещё жив) про Turbo Pascal, а это древность времён MS-DOS.

http://www.borlpasc.narod.ru/docym/br/8/8_15.htm

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

Ловить исключение Вы можете где угодно. Хоть сразу, хоть выше по стеку. В С++ объекты в куче сами собой как раз не уничтожаются. Объекты, размещённые в стеке, нам не интересны.

И всё-таки тут речь идёт об генерации исключения при выделении памяти (или создании объекта в куче) - память кончился - выделение не сработало - значит уже так и так деваться особо не куда - нужно выкручиваться - тут восстановление работы программы априори не может гарантироваться 100% - так что условные "зависания" случаются и на оборудовании - правильно написанный код позволяет откатиться к "началу" - высвободить память и перезапустить основной алгоритм. Исключения позволяют выкручиваться там - где это будет удобно (даже в самом начале всего приложения; и уже тем более грамотно выходить из библиотек), попутно стек будет ещё и раскручиваться и там в файнал блоках могут быть очищены какие-то объекты.

 прошивка ЭБУ выполнила недопустимую операцию и контроллер будет выключен? :)

В этом вся фишка исключений и такого ПО - можно откатиться к его началу и перезапустить. Это куда лучше - чем неверная работа с памятью приведёт к неверной работе всего приложения!

Но я ещё раз подчеркну и выделю: исключения не панацея - можно разрабатывать и без них - и я написал свой комментарий в самом начале не для продвижения исключений, а для описания действий, что можно делать при нехватке памяти - как переходить к нужным алгоритмам - это дело десятое - зависит от архитектуры - но без исключений программный код станет куда сложнее и запутаннее - скорее всего будет куча безусловных/условных переходов, или сложная ручная раскрутка стека и анализ возвращаемых значений - где допустить ошибку будет куда проще! Причём ошибки легко могут быть такими, что не приведут к краху приложения в их месте возникновения - но дальнейшая работа приложения окажется нестабильной и некорректной!

А разве при написании программ бывает иначе?

Просто увеличится уровень ответственности и того, что программист должен будет контролировать самостоятельно - от чего будут расти и риски ошибок программиста из-за человеческого фактора!

В некоторых ситуациях объект исключения тоже выделяет память в момент вызова, поэтому бросить std::bad_alloc не получится - приложение перейдёт в бесконечный цикл аллокации исключения и упадёт с stack overflow.

Как написал выше - вероятность что свободная память будет около нуля - сама около нуля - и хоть немного правильная архитектура программного кода не будет допускать такой ситуации. Но даже если и так - то кризисный код мог быт и такое обработать - просто перед выдачей исключению попытавшись освободить хоть что-то, до чего сможет дотянуться легко и просто - без выделений памяти на куче. Но лучше, конечно, вообще такого не допускать! А если у Вас код без исключений - то у Вас так же будут проблемы с невозможности выделить память на куче для какой-то ручной обработке!

В некоторых ситуациях объект исключения тоже выделяет память в момент вызова, поэтому бросить std::bad_alloc не получится - приложение перейдёт в бесконечный цикл аллокации исключения и упадёт с stack overflow.

Почитайте об embedded разработке, в частности - bare-metal, полезно будет, там нет операционной системы, там нет глобального пула памяти, там невозможны исключения в принципе.

Нет исключений - ну не пользуйтесь - я же не против!

Это не крутость или простота - это одна из областей где С и С++ находят большое применение сейчас.

Да да - Си и C++ безусловно лидеры при низкоуровневой работе с оборудованием , в т.ч. вне классических ОС - и там полно своих нюансов. Ничего не имею против - но всё что я написал в самом начале обсуждения касается и этих систем - если там нет исключений - то и пофиг - всё можно делать и без них

Специально нашёл какой-то древний сайт на народе (оказывается он ещё жив) про Turbo Pascal, а это древность времён MS-DOS.

http://www.borlpasc.narod.ru/docym/br/8/8_15.htm

Так и не смог понять, что Вы этим хотели показать - да эти функции в паскале давно - просто ранее изначально в них не было глобального флага переключателя - мне кажется это уже под конец борланд придумал. А до этого они исключение генерировали - но я могу ошибаться. Но это всё не особо важно!

В С и С++ существует множество исключений, которые программа вообще не сможет поймать средствами языка

Ну.... я не спец по C++ и тут нет дела мне до прочих исключений. Я конкретно говорил только о ручной генерации исключения "Out of memory" и его ручной обработки! Как одной из возможностей выхода их кризисной ситуации когда malloc вернул null (ну или исключение гарнирует оператор new в C++). И файнал блоки тут прекрасно всё отработают при раскрутке стека!

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

Честно - мне всё это пофиг в данном обсуждении - каждому приложению свой подход - я лишь говорил что делать - при выходе на точку программного кода, где будет решаться данная ситуация с нехваткой памяти (или что делать прям сразу - не отходя от "кассы", т.е. от null вернувшегося из malloc).

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

Чушь. Любые объекты, которые были созданы один раз, могут быть созданы повторно. А если некоторый объект не предназначен для повторного создания - то его надо создавать снаружи блока try-catch, только и всего.

Чушь.

Люблю таких категоричных :)

Любые объекты, которые были созданы один раз, могут быть созданы повторно. А если некоторый объект не предназначен для повторного создания - то его надо создавать снаружи блока try-catch, только и всего.

И ведь не ёкнуло от противоречая в своих же словах: всегда можно создать повторно, но если нельзя...

У вас тут спор об особенностях той или иной архитектуры. Он пустой. При любом раскладе всегда можно найти то или иное решение для конкретного случая. А сферическую лошадь в вакууме тут рассматривать бессмысленно!

Но соглашусь с тем что не все объекты всегд можно создать повторно без существенного изменения состояния приложения . Тут просто вопрос иначе надо ставить (для случая кризисной ситуации, в шаге от падения всего приложения):

  1. Что ценнее потерять идентичность какого-то объекта или положить всё приложение?

  2. Можно ли временно выгрузить такой объект (или его важную часть) из памяти, на время восстановления работоспосоности, а потом загрузить её обратно и восстановить идентичность объекта

  3. А правильная ли архитектура работы с такими ценными объектами? Может нужно их держать как-то иначе - чтобы они не терялись в некотором кризисном процессе? В т.ч. вклчив пометку кризисного состояния - не удалять их, при раскрутке стека (хотя тут могут быть организационные сложности при использовании стандартных механизмов отслеживания зоны владения), а сохранять на них ссылку в отдельной области памяти (видимо заранее выделенной), чтобы потом (при, условном, рестарте, не создавать их заново, а взять готовые)

  4. А есть ли ссылки на этот объект (и ссылки внутри объекта) и что с ними делать при его восстановлении?

Никакого противоречия, объект всегда можно создать повторно, но не всегда нужно.

#include <cstdlib>
#include <ctime>
#include <iostream>

class MyObject
{
    public: MyObject()
    {
        init();   
    }
    private: void init()
    {
        _myData = giveData();
    }
    private: int giveData()
    {
        std::srand(std::time(nullptr));  
        return std::rand();
    }
    private: int _myData;
    public: std::string GetData()
    {
        
        return std::to_string(_myData);
    }
};

int main()
{
    MyObject* o = new MyObject();
    std::cout<<"Hello "+o->GetData();
    return 0;
}

Идентичный объект выше стабильно пересоздать будет невозможно, чтобы функция GetData() вернула тот же результат

Где функция giveData() условно может быть любой, связанной с неким неконстантным процессом

Но в данном случает такой объект можно выгрузить и загрузить.

но у поля _myData тут простой числовой тип. Но это мог бы быть некий тип, к конструктору которого вообще нет доступа - данные которого создаёт какой-то "чужой" процесс - хотя останется желание выгрузить загрузить его условно побитово - а если там будут ссылки на другие объекты в куче?

Так что с воссозданием объектов всё действительно не так гладко в С++

А насчёт нужности - представьте что этот объект криптоключ, открывающий доступ ко всем рабочим данным. Действующий некоторое время или в течение некоторого процесса, а потом меняющийся - но получение нового требует передачи старого! Область валидности ключа может быть жёстко ограничена областью кода - в котором криптоключ является ресурсом (получается для использования и возвращается назад при завершении работы), но сбой может выкинуть выполнение из области кода использования ключа (при невозможности корректно обработать все процессы обслуживания ключа, в кризисной ситуации) - а возобновление работоспособности приложения потребует сохранение исходного ключа!

Но тут нет технических ограничений в C++ - нужно лишь правильно выстраивать архитектуру владения ключом (ссылки на объект) , в т.ч. при обработке сбоев!

А зачем вам идентичный-то объект? Нужен не идентичный, а работающий.

Да, пересоздание этого объекта приведёт к повторному запросу данных, но ведь в этом и суть! Нужны новые данные - создаём новый объект, не нужны новые данные - не создаём его.

Дополнил комментарий (хотя и так должно быть понятно), прочитайте ещё раз. Если по-прежнему не понятно почему - я навряд ли до Вас доведу эту мысль

Хотя... ну вот другой пример без кода:

У Вас приложение взаимодействует скажем с онлайн магазином билетов - вы купили билет и потратили на него деньги - получили билет в виде электронного объекта. Утрата объекта = утрата билета. Да - Вы можете получить новый билет из магазина - но Вам снова придётся потратить деньги. Билеты не будут идентичны - и могут даже выполнять одну и ту же функцию, но Вы потратили повторно деньги.

А если это лотерейные билеты - то вопрос выполнения одной и той же функции становится острее - вроде бы одна и та же: функция вероятности получения выигрыша (и эта вероятность от вашего приложения даже не зависит) - вот только это два разных билета - и факт в том, что любой из них в какой-то момент может выиграть - первый или второй! Но первый уже безвозвратно потерян... А за второй Вы и так уже заплатили повторно!

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

Вот вы пишете - "возвращается назад при завершении работы", что вам мешает точно так же вернуть его назад при исключении?

Тут какое-то непонимание ситуации. Что мы вообще обсуждаем?

Я так понял обсуждаем то, что объект может являться ресурсом, который создаётся на время выполнения какого-то кода - после чего финалится и освобождается

Но возникновение исключения не позврляет отрабоать всей секции кода до конца - и объект может быть зафинален и освобождён. Но затем мы делаем восстановление роботоспособности - и нам нужен этот самый объект для продолжения работы.

Выше я рассказал и то, как можно поступать в таких случаях

И то, что не все объекты можно вот так взять и пересоздать!

Безусловно - всё что можно пересоздать - можно и пересоздать! Но, даже, например, при взаимодействии с СУБД - не факт, что новая выборка Вам вернёт те же данные. А Вам нужны исходные - чтобы завершить начатый процесс, который если корректно не завершить со старыми данными - то хрен знает что случится тогда!

То есть, с некоторыми данными действительно надо быть аккуратными и не терять! Даже если формально есть процесс получения новых! Вот только есть ли уверенность на 100% что всё будет стабильно при повторном получении?

 что вам мешает точно так же вернуть его назад при исключении?

А вот это я не вообще не понял. Напомню - что тут обсуждение работы в кризисной ситуации - в частности ситуации нехватки памяти!

Но затем мы делаем восстановление роботоспособности - и нам нужен этот самый объект для продолжения работы.

Если нам и правда нужен живой объект - надо перехватить исключение до того как он будет освобождён, только и всего. Зачем придумывать какую-то ненадёжную процедуру восстановления объекта обратно?

Надо перехватить исключение до того как он будет освобождён, только и всего

А вот это не всегда архитектурно удобно! И фактически отменяет более половины всех преимуществ исключений. Это уже другая тема

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

А зачем вам идентичный-то объект? Нужен не идентичный, а работающий.

Простой пример - TCP сокет как результат вызова функции accept(). Повторный вызов вернёт другой сокет (и неизвестно через какое время), а соединение с клиентом будет потеряно.

Повторный вызов вернёт другой сокет (и неизвестно через какое время), а соединение с клиентом будет потеряно.

…и это правильное поведение при возникновении неустранимой ошибки при обработке соединения.

Формально согласен - при восстановлении соединения номер сокета не должен играть ключевую роль - иначе - это архитектурная ошибка. Другое дело - что если удалённая сторона и её архитектура от Вас никак не зависит, и приходится работать с тем что есть

Но - тут сохранить номер сокета вообще не проблема даже при нехватке памяти

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

А файнал блоки доделают остальную работу - если будет исключение, которые Вы тут обрабатывать не готовы.

Отвечу отдельно.

В С и С++ существует множество исключений, которые программа вообще не сможет поймать средствами языка. Для примера - SEH исключения. Поймать SEH исключение возможно только специальными расширениями от Microsoft, соответственно это будет работать только для компилятора из MSVC и не переносимо не то, что на другую платформу, но даже на, к примеру, clang или MinGW.

Так что, в общем случае - finally блока в С++ нет вообще.

Может быть и так, но какие есть альтернативы?

Тот же Rust с памятью обращается более бережно. Как и Go (там свой менеджер памяти) или Kotlin Native (тут вроде бы тоже свой менеджер памяти)

Ну и многие современные ЯП (и ряд старых) тоже аккуратнее относятся к выделению памяти - не возвращая ноль - а генерируя исключение - а его уже проигнорировать будет куда сложнее!

То есть - это по большей части проблема Си и чуть меньше C++ (где правильно выделять память через оператор new - а он, вроде бы генерирует исключение, но я, честно, точно не помню этого)

Go и Kotlin точно не являются альтернативами Си, т.к. у них есть GC. Rust вполне можно использовать, как замену.

То есть - это по большей части проблема Си и чуть меньше C++ (где правильно выделять память через оператор new.

Насколько знаю, сейчас уже принято использовать unique_ptr и shared_ptr вместо чистого new, так что даже об освобождении думать не нужно. В целом некоторые чисто сишные проблемы в C++ уже решили, так что я бы его выбрал, но там всё равно есть свои заморочки с UB, такие же, как в си

Go и Kotlin точно не являются альтернативами Си, т.к. у них есть 

Ну так Вам более защищённая альтернатива нужна - с управлением памятью - или полная свобода маньяка-мазохиста, где кодинг - это хождение по краю пропасти!

У Rust тоже есть управление памятью - но да - переложенное на плечи программиста и компилятора - маньякам-мазохистам понравится!

В Kotlin Native, кстати вроде не GC а подсчёт ссылок. Да и новая модель работы с памятью там есть - правда пока не разбирался как устроена - но вроде бы тоже что-то на основе владения

Ну так Вам более защищённая альтернатива нужна - с управлением памятью - или полная свобода маньяка-мазохиста, где кодинг - это хождение по краю пропасти!

Мы явно друг друга не поняли. Там где можно взять Go или Kotlin не стоит вопроса брать или не брать C, C++. Ответ однозначный - не брать.

У Rust тоже есть управление памятью - но да - переложенное на плечи
программиста и компилятора - маньякам-мазохистам понравится!

Ну не знаю, тут совсем надо быть мазохистом

Мне одному кажется, что язык программирования, требующий таких танцев с бубном, морально и физически устарел?

Не дождётесь.

«Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует» Bjarne Stroustrup
:)

Обработать ошибку - это теперь "танцы с бубном" называется?

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий