Pull to refresh

Comments 41

ИМХО перепутаны два понятия, согласованность состояния и освобождение ресурсов.

1) То что Dispose() не будет вызван при возникновении исключения в конструкторе мешает освобождать ресурсы своевременно, но не мешает поддерживать согласованное состояние программы.

2) Dispose(), в отличие от деструктора Си++, обычный метод который можно вызвать обычным образом. Ничто не мешает вручную написать try/finally вручную вызывать Dispose() самостоятельно. Более того, try/catch можно написать в конструкторе и вызвать Dispose() из catch. причём одно не исключает друогого, так как Dispose() по спецификации может быть вызван многократно.

3) Ну и самое главное замечание, можно сказать что в статье есть фактическая ошибка. В C# есть то, чего нет в Си++, Constrained Execution Regions. Это как раз возможность множество операций объединить в непрерываемый исключениями блок операций. Так что далеко не только присваивание ссылки может быть использовано.
На самом деле я хотел лишь сказать, что утечки ресурсов есть лишь частный случай рассогласованного состояния приложения. Но вот перепутанность понятий я не заметил. Ни по одному из приведенных тобой пунктов у меня нет никаких возражений, поскольку все это так и есть. И об этом, вроде как и идет речь. Разве нет?

По поводу 1): Да, так и есть и я об этом же и пишу, вот пруф из статьи:
В язык C# эта идиома (речь идет о RAII) перекочевала в виде интерфейса IDisposable и конструкции using, однако, в отличие от С++ она применима для управления временем жизни ресурса в некоторой области видимости, и не подходит для управления множеством ресурсов, захватываемых в конструкторе.

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

По поводу 2) Да, так и есть, вот еще один пруф:
Все, что нам остается, это либо создавать подобъекты в нужном порядке и disposable поле создавать в самом конце конструктора, либо оборачивать их создание в блок try/catch и очищать все ресурсы, в случае возникновения исключения.


По поводу 3) Да, в .Net-е действительно есть Constrained Execution Regions, которые способны обеспечить все рассматриваемые в статье гарантии, не только для синхронных исключений, но и для асинхронных. Просто вопрос в том, что в прикладном приложении усилия обеспечения таких гарантий просто не окупятся, поскольку большинство классов .Net Framework таких гарантий для асинхронных исключений не предоставляют.

Пруф по этому поводу находится в одной из ссылок, в приведенной заметке «О вреде метода Thread.Abort», а именно в статье Джо Даффи Atomicity and asynchronous exception failures, в которой он пишет следующее:
We don’t expect most of the Framework to be willing or able to recover fully from asynchronous exception.

З.Ы. За фидбек спасибо!
Ну вот смотри. У тебя написано “при возникновении любого исключения в некотором методе, состояние программы должно оставаться согласованным” и в качестве примера для нарушения базовой гарантии использование IDisposable и отсутствие вызова Dispose().

Я бы привёл другой пример, где исключение вызывает не отсутствие вызова Dispose(), а его неправильную работу, из-за несогласованности полей класса. Собственно, это как раз то для чего нужен CER. Что-то вроде

class DisposableX: IDisposable
{

DisposableX()
{
_handleA = OpenHandle(...);
// Тут исключение по любой причине.
_handleB = OpenHandle(_handleA, ...);
}

void Dispose()
{
CloseHandle(_handleB); // ага, сейчас, он же IntPtr.Zero.
CloseHandle(_handleA);
}

}
Да, согласен, возможно пример и не самый удачный, но осознанный. Во-первых, хотел показать, что даже в таком высокоуровневом языке как C# можно довольно легко допустить утечки ресурсов. Кроме того, я упоминал пример имено рассогласования состояния приложения, когда при переводе денег с одного счета на другой мы получаем рассогласованное состояние из-за исключения.

Возможно стоило привести подобный пример:

public void TransferMoney(Acount anotherAccount, Decimal amount)
{
this.Withdraw(amount);
// Если вторая часть операции упадет, то получим рассогласованное состояние
anotherAccount.Deposit(amount);
}


Но он показался уж слишком банальным.

Кстати, твой пример не совсем подходит, поскольку твой объект работает с неуправляемыми ресурсами, а в этом случае нам понадобится финализатор, который будет вызван даже если конструктор объекта «упадет» (это еще одно важное отличие между С++ и C#). Так что в твоем случае, достаточно корректно очистить ресурсы в финализаторе (вызвав что-то вроде Dispose(false)), и сделать нечто подобное (хотя, мы опять таки получим недетерминированный характер очистки):

~DisposableX() {
}

private void Dispose(bool disposing)
{
// В общем, на этот флаг нам, в общем-то плевать, поскольку мы работаем только
// с неуправляемыми ресурсами...
if (_handleA != IntPtr.Zero)
CloseHandle(_handleA);
if (_handleB != IntPtr.Zero)
CloseHandle(_handleB);
}


В общем, с неуправляемыми ресурсами, все же несколько другая песня. Да и юзаются они значительно реже, нежели управляемые.
к 3му пункту — причём, если правильно помню, там ещё можно ещё до входа в try/catch блок проверить, хватит ли ресурсов для работы метода (это не точная проверка, конечно, но определённые гарантии даёт).
Это только для выделения здоровенных кусков памяти относится. Речь идет о классе MemoryFailPoint.
__Микрософт не рекомендует использовать var нигде кроме как в случае анонимных типов.
Пруф в студию, пожалуйста. А то я как ни гляну в блог Эрика, так он все о противоположном пишет чего-то. Видно не понимает ничего в этом:) Вот, кстати, отличная статья об этом: Uses and misuses of implicit typing и вот она же на русском.

А если серьезно, какое это имеет отношение к рассматриваемой теме?
Скорее они рекомендуют использовать его там, где явно указывается тип возвращаемого значения:

var sr = new StreamReader();

но не

var sr = GetSomeCustomReader();
Пруфлинк:
> The var keyword can also be useful when the specific type of the variable is tedious to type on the keyboard, or is obvious, or does not add to the readability of the code. One example where var is helpful in this manner is with nested generic types such as those used with group operations. In the following query, the type of the query variable is IEnumerable<IGrouping<string, Student>>. As long as you and others who must maintain your code understand this, there is no problem with using implicit typing for convenience and brevity.
Я, все же, не могу с этим полностью согласиться по нескольким причинам:
Во-первых, следуя этому пруфу в моем коде нет неверного использования var-а, поскольку все типы «obvious».
Во-вторых, если следовать такой логике, то программировать на F# вообще невозможно, поскольку там типы явно не указываются.
В-третьих, MSDN — далеко не лучший советчик. Просто посмотрите на примеры кода для большинства классов и попробуйте их использовать без модификаций в своих продуктах, объясняя это тем, что в MSDN-е так написано. У меня следующие авторитеты: здравый смысл, собственный опыт, Липпер, Скитт и им подобные. Анонимусы в MSDN-е идут в конце списка.
В-четвертых, как показывает практика, зачастую дополнительная информация о типе легко доступна (достаточно подвести мышару к var-у), и не несет дополнительной полезной информации. Я предпочитаю такое правило (кстати, именно оно записано в нашем корпоративном Style guideline): используйте var всегда, и используйте явную типизацию только тогда, когда без этого читать код становится проблематично. Практика показывает, что в большинстве случаев использование var-а нисколько не конфьюзит читателя.
В-пятых, это тотальный офтоп, который не имеет ни малейшего отношения к рассматриваемой теме:)
Не лучше ли было бы явно указать тип в этой строчке?
var person = tmpPerson;

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

Извиняюсь, что чуть не по теме.
Как, собственно, пишет Липперт, совершенно не важно, какого типа переменная, важно, что она хранит.

«Если давать имена переменным с типом в имени»
Это не «тип в имени», это конкретный объект. person — это не «объект типа Person», это «персона».
Да, здесь много тонкостей, спасибо за хороший обзор.

Насколько я знаю, рекомендуется не захватывать ресурсы в конструкторе. Конструктор должен только создавать (конструировать) объект. (Кажется, это ещё в «Совершенном коде» рекомендовалось. Не помню точно.)
Пример такого подхода — SqlConnection: при создании объекта реальное соединение с базой данных не будет установлено, для этого нужно сначала вызвать Open().

В случае, если нужно в одной строчке создавать объект и захватывать ресурс, лучше использовать статические методы или методы другого класса. Пример — File.Open().

Такой подход позволит упростить работу с ресурсами и снизить риск утечек.
На мой взгляд довольно хорошее и простое решение
Спасибо. Единственное, возник вопрос по поводу асинхронных исключений:
Все сказанное в этой статье относится только к синхронным исключениям, поскольку гарантировать согласованное возникновении «асинхронных» исключений, таких как OutOfMemoryException или ThreadAbortException практически невозможно.

Имеется ввиду то, что гарантировать отсутствие таких исключений нельзя, поскольку возникнуть они могут практически в любой момент?
А, разобрался уже. Дело в том, что такие исключения могут происходить между инструкицями присваивания, например, за счет чего даже стандартные конструкции типа
using (Obj obj = new Obj()
{
}
могут быть небезопасными.
Да, совершенно верно. Мы не можем обеспечить сами даже базовую гарантию безопасности асинхронных исклюений, поскольку основные строительные блоки, на основе которых строится наше приложение, такую гарантию не обеспечивают. Речь идет о классах .Net Framework, которые могут оказаться в рассогласованном состоянии из-за возникновения асинхронного исключения в процессе их работы.
кто-нибудь может сказать чем же закончились ушедшие в прошлое баталии «исключения vs коды ошибок»?
Я юзаю исключения или кастомные исключения, но на работе часто требуют, чтобы я не обрабатывал ошибки с помощью исключений (разумеется, я это делаю в разумных пределах), только какой-то фатал эрор или нал-референс в аргументе. Хотя у нас остается некорректные значения аргументов, ненайденные объекты в базе, которые не позволяют продолжать нормальный процесс работы, ошибки валидации и т.д.
на MSDN пишут, что исключения применять нужно очень избирательно и именно эта фраза вводит в заблуждение: где границы той избирательности на практике.
Очень бы хотелось именно примеров и экспертных мнений
«кто-нибудь может сказать чем же закончились ушедшие в прошлое баталии «исключения vs коды ошибок»?»
В .net все очень просто — только исключения. Просто потому, что попробуйте найти стандартную функцию в core library, которая возвращает код ошибки, а не бросает исключение.
Как мне назвали проблему: в случае исключения может падать IIS а его подъем — тяжка задача для системы (возможно я сформулировал несколько криво, уже не помню точно, но смысь примерно такой была), а вот с техлидом не поспоришь, хотя я лично замерял скорость обработки исключений vs кодов ошибок (return 1,2,3)
Да, отличие от 5 до 100 раз в зависимости от кол-во исключений/секунду, но ни кто не будет генерировать 10000-20000 исключений в секунду в нормальном режиме и во-вторых эти 10 000 исключений обрабатываются за 0,002 секунды.
«в случае исключения может падать IIS»
Обманули. Ничего никуда не падает. Не говоря уже о том, что исключения можно (и нужно) обрабатывать.
Не обманули, скорее — очень раздули проблему. Например, исключение на потоке из пула убивает IIS, точнее, процесс, обслуживающий Application Pool:
ThreadPool.QueueUserWorkItem(delegate { throw new Exception(); });

И тут же в логах: Faulting application name: w3wp.exe, version: 7.5.7601.17514, time stamp: 0x4ce7afa2…

Но это не настолько серьезный аргумент, чтобы переходить с исключений на коды ошибок.
Оооо, потоки из пула. Те самые, которые на iis вообще использовать не рекомендуется, если мне память не изменяет.
Где и кем не рекомендуется? В MSDN? Покажите ссылку, пожалуйста. Вы же не гуглите каждый метод CLR на предмет безопасности под IIS?
Пример валит IIS, в статье по QueueUserWorkItem предупреждения нет — этого достаточно, чтобы кто-то наступил на грабли.

Стандартный TransactionScope использует потоки из пула для реализации таймаутов. Любой зарегистрированный в нем ресурс может кинуть исключение в момент rollback-а, и точно так же утянет за собой w3wp. И, да, я видел такое на своем проекте, на живой системе. TransactionScope тоже не рекомендуется использовать?
«Где и кем не рекомендуется?»
Здравым смыслом. Мы аккурат на той неделе это гуглили (да, именно этот метод на пример безопасности под IIS), потому что где-то в подкорке есть опасения относительно многопоточности (любой) в IIS. И были правы, потому что использование тредпула может привести к голоданию обработчика запросов iis, что не благо совсем.

То, что вы привели — красивый, показательный и очень полезный пример, который надо держать в голове, да. Спасибо.

Но тем не менее самостоятельно использовать потоки в web я все равно не буду.

(Ну и IIS-то все равно не падает)
Мне здравый смысл подсказывал, что использовать TransactionScope — безопасно. У вас есть 100% уверенность, что в приложении косвенно не используется пул? Где граница использования потоков? TPL можно использовать? PLINQ? Как насчет стандартных Async Pages?
При исключении в таске в TPL — приложение падает при вызове финализатора, что еще интереснее в отладке.

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

>> (Ну и IIS-то все равно не падает)
Я же проверил перед тем, как написать комментарий. И DevServer, и IIS7 под .NET 4 Integrated Mode падают при вписывании строчки в Page_Load на стандартном шаблоне сайта.
«Где граница использования потоков?»
Там, где начинаются потоки, а не IOCP. Последнее время я пребываю в устойчивой уверенности, что никакую обработку на IIS параллелить не надо, чтобы не трогать его бедный процесс обработки входящих. Если надо что-то реально параллелить — в отдельный сервис, с которым асинхронно общаться. А обычно там ничего настолько cpu-bound не бывает, наибольшая часть операций — IO-bound, а эти надо вешать на IOCP.

«Я же проверил перед тем, как написать комментарий. И DevServer, и IIS7 под .NET 4 Integrated Mode падают при вписывании строчки в Page_Load на стандартном шаблоне сайта.»
Как вы же написали выше, падает не IIS, а процесс, обслуживающий этот apppool. Некоторая разница.
На самом деле, до меня внезапно дошло, что, собственно, мы тут на пустом месте это обсуждаем, потому что как раз в порожденных потоках исключения использовать рекомендуется очень аккуратно (то есть, ловить).

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

(впрочем, я выше писал — «исключения можно (и нужно) обрабатывать»)
обрабатывать нужно. Вот это «считается» и нагружает объекты
Я, если честно, не понял, про какое «считается» вы говорите, и что нагружает объекты.

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

При этом на самом верхнем уровне все исключения ловятся, аккуратно обрабатываются и кладутся куда положено.
Хмм, странная информация… в случае исключения в веб-приложении никакой IIS естественно не падает :)
речь идёт о пуле приложений в IIS. PashaPash верно определил
Я всё чаще испытываю чувство, что история развития языков и методологий программирования сделала очень странный виток. Этот пост — тому яркая иллюстрация.Обработка исключений. Господа, это уже не смешно, то что вы тут обсуждаете — это азбука Delphi. Все это было придумано, обсуждено, пережевано, осмысленно и внедрено более 10 лет назад.
Не ради стебу, а справедливости для. Исходная статья Абрахамса датирована 1996-м годом (т.е. 15 лет назад), проектированию по контракту более 20 лет, функциональщине более 50-ти. Но это не мешает нам переоткрывать все эти вещи для себя.

Кстати, напомню, что здесь речь идет не просто об «обработке исключений», а о гарантии безопасности исключений. Так что, если не затруднит, скиньте пруфы на «азбуку Delphi», в которой обсуждаются именно гарантии безопасности исключений?

З.Ы. Напомню, что в среде С++ программистов — это старый и известный баян, но вот в среде C# программистов эта тема далеко не так распространена.
Гарантии безопасности в Delphi не обсуждаются, т.к. VCL исповедует одну единственную строгую гарантию, хотя явно об этом не говорится. За внешние ресурсы и библиотеки конечно никто поручится не сможет.
В зависимости от квалификации програмиста программа может плавать от полного отсутствия гарантий вообще (говнокод) до строгой гарантии.

Что касается обработки исключений, то достаточно знать всего 3 правила:
1. Любая инструкция может вызвать исключение
1.1. Следовательно, выделение ресурсов и работа с ними должна находится в секции try, а освобождение — в секции finally
2. При возникновении исключения в конструкторе будет автоматически вызван деструктор.
3. Вызов .Free у нулевого объекта не вызовет исключение.

Всё! Следование этим правилам на всех уровня приложения даёт как минимум базовую гарантию безопасности исключений.
Троллинг детектед:)

Базовая гарантия исключений не достигается банальным растыкиванием блоков try/catch/finally.

Банальный пример:

// Метод класса Account
public void Transfer(Account account, Decimal value)
{
try {
Withdraw(value);
account.Deposit(value);
}
catch(Exception e)
{
// А!!! Че делать! А давайте просто запишем ошибку в лог или пробросим ее дальше
log.WriteError(e);
throw;
}
}
Думаете такого не бывает? Я встречаю такой код постоянно. Так вот, исключения здесь обрабатываются, но базовая гарантия исключений не соблюдается.

З.Ы. Повторюсь еще раз. В статье речь не только и не столько об управлении ресурсами, сколько о гарантиях обеспечения согласованного состояния приложения (что есть более общий случай).
Даже не планировал вас сейчас троллить, честное слово :)

Кстати а где в приведенном коде утечка ресурсов? Я не слишком силен в С#.

PS: да, ваша правда, статья несколько шире
> З.Ы. Повторюсь еще раз. В статье речь не только и не столько об управлении ресурсами, сколько о гарантиях обеспечения согласованного состояния приложения (что есть более общий случай).

Я привел пример, когда мы не получаем утечку ресурсов, но получаем рассогласованное состояние приложения, что и есть нарушение базовой гарантии безопасности исключений. (и что более важно в общем случае)
Так в вашем примере неправильно обработка исключений производится. в секции catch надо было устранить рассогласование. Или не помещать туда withdraw. Кстати что это за метод?
Only those users with full accounts are able to leave comments. Log in, please.