Подтвердить что ты не робот

Является ли С# использование оператора безопасным?

Я только что закончил читать "С# 4.0 в двух словах" (O'Reilly), и я думаю, что это отличная книга для программиста, желающего перейти на С#, но это оставило меня в недоумении. Моя проблема заключается в определении оператора using. Согласно книге (стр. 138),

using (StreamReader reader = File.OpenText("file.txt")) {
    ...
}

точно эквивалентен:

StreamReader reader = File.OpenText("file.txt");
try {
    ...
} finally {
    if (reader != null)
        ((IDisposable)reader).Dispose();
}

Предположим, однако, что это верно и этот код выполняется в отдельном потоке. Этот поток теперь прерывается с помощью thread.Abort(), поэтому создается ThreadAbortException, и пусть поток точно после инициализации читателя и перед вступлением в предложение try..finally. Это будет означать, что читатель не настроен!

Возможное решение было бы следующим образом:

StreamReader reader = null;
try {
    reader = File.OpenText("file.txt");
    ...
} finally {
    if (reader != null)
        ((IDisposable)reader).Dispose();
}

Это будет безопасно.

Теперь для моих вопросов:

  • Правильно ли авторы книги и оператор using не являются безопасными или ошибочны, и они ведут себя как в моем втором решении?
  • Если using эквивалентно первому варианту (не прерывается), почему он проверяет наличие null в finally?
  • Согласно книге (стр. 856), ThreadAbortException можно выбросить в любом месте управляемого кода. Но, может быть, есть исключения, и первый вариант в любом случае прерван?

EDIT: Я знаю, что использование thread.Abort() не считается хорошей практикой. Мой интерес является чисто теоретическим: как оператор using ведет себя точно?

4b9b3361

Ответ 1

На веб-сайте книжного компаньона есть дополнительная информация о прерывании потоков здесь.

Короче говоря, первый перевод правильный (вы можете сказать, посмотрев на ИЛ).

Ответ на ваш второй вопрос заключается в том, что могут быть сценарии, в которых переменная может быть законно нулевой. Например, GetFoo() может возвращать значение null здесь, в котором вам не нужно исключать NullReferenceException в неявный блок finally:

using (var x = GetFoo())
{
   ...
}

Чтобы ответить на ваш третий вопрос, единственный способ сделать Abort безопасным (если вы вызываете код Framework) - это снести AppDomain после этого. Это фактически практическое решение во многих случаях (это именно то, что делает LINQPad, когда вы отменяете выполняемый запрос).

Ответ 2

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

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

В ответ на ваше редактирование - я бы еще раз отметил, что ваши два сценария на самом деле идентичны. Переменная "reader" будет равна null, если только вызов File.OpenText не завершится успешно и не вернет значение, поэтому нет никакой разницы между написанием кода из первого способа и вторым.

Ответ 3

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

Исключения обычно разворачиваются чисто, но в крайних случаях нет гарантии, что каждый бит кода может выполняться. Более наглядным примером является "что произойдет, если сбой питания?".

Повторите проверку null; что, если File.OpenText вернулся null? Хорошо, это не будет, но компилятор не знает этого.

Ответ 4

Немного offtopic, но поведение оператора блокировки во время абортов потоков также интересно. Хотя блокировка эквивалентна:

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
    …
}
finally {
    System.Threading.Monitor.Exit(obj);
}

Гарантируется (с помощью x86 JITter), что прерывание потока не происходит между Monitor.Enter и выражением try.
http://blogs.msdn.com/b/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx

Сгенерированный код IL, по-видимому, отличается от .net 4:
http://blogs.msdn.com/b/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx

Ответ 5

Спецификация языка четко указывает, что первая правильная.

http://msdn.microsoft.com/en-us/vcsharp/aa336809.aspx MS Spec (документ Word)
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf Спецификация ECMA

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

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

Ответ 6

Вы сосредотачиваетесь на неправильной проблеме. Исключение ThreadAbortException так же вероятно отменяет метод OpenText(). Вы можете надеяться, что он устойчив к этому, но это не так. В методах framework нет предложений try/catch, которые пытаются справиться с прерыванием потока.

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

Ответ 7

Являются ли авторы книги правильными, а оператор using не является безопасным или неправильным, и он ведет себя как в моем втором решении?

Согласно книге (стр. 856), ThreadAbortException может быть выброшено в любом месте управляемого кода. Но, может быть, есть исключения, и первый вариант в любом случае прерван?

Авторы правы. Блок using не отменяется. Второе решение также не отменено, поток может быть прерван в середине сбора ресурсов.

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

A Thread.Abort будет ждать только кода, выполняющегося внутри областей с ограничениями выполнения (CER), finally, блоков catch, статических конструкторов и неуправляемого кода. Таким образом, это безопасное решение (только в отношении приобретения и утилизации ресурса):

StreamReader reader = null;
try {
  try { }
  finally { reader = File.OpenText("file.txt"); }
  // ...
}
finally {
  if (reader != null) reader.Dispose();
}

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

Если использование эквивалентно первому варианту (не прерывается), почему он в конечном итоге проверяет значение null?

Проверка на нуль делает шаблон using безопасным при наличии ссылок null.

Ответ 8

Первое действительно эквивалентно последнему.

Как уже указывалось, ThreadAbort действительно плохо, но это не то же самое, что убить задачу с помощью диспетчера задач или отключить компьютер.

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

Тем не менее, если вы в ThreadAbort, зачем пытаться очистить? Вы все равно в смерти.

Ответ 9

команда finally всегда выполняется, MSDN говорит: "Наконец, используется, чтобы гарантировать, что блок выполнения кода выполняется независимо от того, как предшествующий блок try."

Таким образом, вам не нужно беспокоиться о том, что не очищать ресурсы и т.д. (только если окна, Framework-Runtime или что-то еще плохое, вы не можете контролировать, но тогда есть больше проблем, чем очистка ресурсов;-))