Недавно на работе нам
прислали несколько “интересных ссылок по языку C#”. После перехода по соответствующей ссылке я ужаснулся тому, как можно писать статьи, туго понимая их суть. До этого мне приходилось
читать оригинал других нелепых вещей данного автора и критику этих материалов Сергеем Тепляковым ("8 наиболее распространенных ошибок C# программистов"). После этого я перестал читать
блог компании Goyello.
Но после вчерашнего прочтения и будучи под неприятным впечатлением, не могу пройти мимо статьи Threading
in C#: 7 Things you should always remember about. Перед градом субъективной критики этого материала рекомендую ознакомиться с ним.
Приступим к разбору ошибок.
Threads share data if they have a common reference to the same object
instance
С первого взгляда
насчет приведенного автором кода не возникает сомнений. Вот исходный код:
class SomeClass
{
private bool _isWorkDone;
static void Main(string[] args)
{
SomeClass someClass = new SomeClass();
Thread newThread = new Thread(someClass.DoWork);
newThread.Start();
someClass.DoWork();
Console.Read();
}
void DoWork()
{
if (!_isWorkDone)
{
_isWorkDone = true;
Console.WriteLine("Work
done");
}
}
}
Рассмотрим
это код немного ближе. Автор пишет, что проблема в том, что программист ожидает
выполнения кода дважды, а код выполнится только один раз. На мой взгляд, этот
факт бесполезный. Сложно представить программиста, вызывающего функцию одного и
того же класса с надеждой на то, что переменная каким-то образом изменится.
‘Finally’ blocks
in background threads are not executed when the process terminates
Здравый
совет. Только автор об этом говорит одним предложением, и у начинающих
разработчиков создается предположение, что главное - это не забыть избегать
непредвиденного выхода из программы. Автор, вроде, и дает совет, но на практике
он не принесет никакой пользы. Объясню почему: во-первых, потому что
прерывание выполнения приложения может произойти несколькими способами.
Во-вторых, после перечисленных выше способов Вам от блока finally не
будет никакой пользы. Одной из ситуаций, в которых не отработает блок finally,
есть ситуация, в которой в блоке try происходит вызов
бесконечного цикла. Иногда нужно иметь возможность корректно завершить
приложение самому, записав при этом информацию в event log. В .NET 4.0
появился метод Environment.FailFast,
который позволяет завершить процесс сразу после записи сообщения в журнал
событий Windows,
после чего включает сообщение в отчет об ошибках, отправляемый в корпорацию
Майкрософт. Этот метод завершает процесс, минуя активные блоки try/finally
и
методы-финализаторы. Метод FailFast
записывает
строку message
в
журнале событий Windows-приложения,
создает дамп приложения и затем завершает текущий процесс. Строка message
также включена в отчет об ошибках
для корпорации Майкрософт. Метод FailFast
используется
для завершения работы приложения вместо метода Exit,
если повреждение состояния приложения не подлежит восстановлению, а выполнение
блоков try/finally
и
методов завершения для этого приложения приведет к повреждению ресурсов
программы. Совет автора звучит так: "помните о том, что Ваш блок finally
может
быть не вызван". На такой тип сообщения хочется
ответить: "Спасибо, К.О.". Этот совет из разряда "Ваша
программа может упасть, знайте об этом". С таким подходом к описанию
проблемы этот совет можно отнести к таким, от которых польза равна нулю. Поэтому
позволю себе дать несколько советов, на правах автора данной статьи.
1.
Постарайтесь избегать в программе, если она часто использует выход с помощью
методов Environment.Exit или
Environment.FailFast,
управление нативными
ресурсами, так как это может привести к их повреждению. В большинстве же
случаев ваши ресурсы корректно будут освобождены после того закрытия
приложения.
2.
С такими типами ошибок, как, например, OverflowException, при которых блок finally не
отрабатывает, бороться намного сложнее. Если у Вас выпало приложение с OverflowException, то нужно смотреть в
корень проблемы, а совет, как здесь позаботиться об освобождении ресурсов, не
совсем уместный. Поскольку C#
-
управляемый
язык, то в 95% случаев у Вас не будет краха ресурсов при нормальном написании
кода. В остальных 5% нужен глубокий анализ кода и понимание причины
воспроизведения такой ошибки.
Captured values
in lambda expressions are shared as well
Еще
один бессмысленный пример, который не имеет никакого отношения к потокам. Если
бы автор переписал этот пример с помощью цикла foreach, то при запуске в .NET 4/4.5
он увидел бы тот результат, который он хотел получить. То, что описал автор
статьи, называется closure
variables
или
замыкание на переменных цикла. Более детально о замыканиях можно почитать в
статьях Сергея Теплякова "Замыкание на
переменных цикла в C# 5.0" и Замыкания в языке
программирования C# или посмотреть эту информацию в блоге Эрика
Липпера/Джона Скита. Я же приведу пример использования цикла foreach, в котором пример
автора сработает.
static void Main(string[] args)
{
var result = Enumerable.Range(0, 10);
foreach (var i in result)
{
Thread thread = new Thread(() => Console.Write(i));
thread.Start();
}
Console.ReadLine();
}
Locking does not
restrict access to the synchronizing object itself in any way.
Этот
совет можно назвать так: "Я никогда не смотрю в MSDN и
это для меня проблема". С типом x, который использует автор, вообще ничего не
понятно. Какого типа у нас x
и
что вообще автор пытался нам донести. Лично я перечитал этот пример несколько
раз, но так и не понял, какую мысль читателям хотел донести автор. Не
будем гадать и посмотрим, что по этому поводу говорится в MSDN:
Как правило, рекомендуется избегать
блокировки типа public или экземпляров, которыми код не управляет.
Распространенные конструкции lock (this), lock(typeof(MyType)) и
lock("myLock") не соблюдают это правило.
- lock (this) может привести к проблеме, если к экземпляру допускается открытый доступ.
- lock (typeof(MyType))может привести к проблеме, если к MyType допускается открытый доступ.
- lock("myLock")может привести к проблеме, поскольку любой код в процессе, используя ту же строку, будет совместно использовать ту же блокировку.
Складывается
впечатление, что автор неуклюже пытается донести то, что сказано в MSDN,
соответственно и насчет его примера, поскольку я не представляю программиста,
который написал бы так, разве что кроме автора исходной статьи.
Try/catch/finally blocks in scope when a thread is
created, are of no relevance to the thread when it starts executing
Этот совет из разряда
советов, не поддающихся логике. Для новичков он бесполезен, а для программистов с опытом он вызовет когнитивный диссонанс.
Новички могут подумать, что если они перепишут код следующим образом,
static void Main(string[] args)
{
try
{
Thread thread = new Thread(() => Divide(10, 0));
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine("An
exception occured");
}
Console.ReadLine();
}
static void Divide(int x, int y)
{
int z = x / y;
}
то
пример заработает. Это не так. Ошибка будет вызвана не в основном потоке,
поэтому она не будет перехвачена. Решение о перемещении блока try->catch в
метод Devide
-
хороший совет, но способ преподнесения автором советов начинает вводить в
ступор. Пожалуй, немного расширю эту тему и расскажу о том, что автор исходной
статьи опустил. Начнем из того, что есть возможность перекинуть ошибку в
основной поток. Это можно сделать, дожидаясь ответа от дополнительного потока с
помощью вызова метода Join().
Второй способ -
использовать новые возможности C#
5.0 с классом ExceptionDispatchInfo.
Третья возможность заключается в использовании модели Asynchronous
Programming Model (APM). Четвертая – использование Task
Parallel Library (TPL). Кроме способа, предложенного автором о переносе
блока try->catch с
одного места в другое, можно рассмотреть альтернативы, которые решают ситуацию
лучшим образом. У меня осталось двоякое впечатление после этой рекомендации. С
одной стороны, потому что совет, вроде, правильный, а с другой -
он логичный и "капитанский". Поэтому просматривайте такие статьи
осторожно и следуйте таким советам только с умом.
If an object is
thread-safe it does not imply that you don’t need to lock around accessing it
Мне
сложно назвать этот совет полезным. Дело в том, что об этом пишет компания Microsoft, а также известные
авторы Рихтер, Скит, Троелсен и т.д. Они говорят о том, что коллекции в .NET не
являются потокобезопасными. Если нужна безопасность для коллекций, можно
воспользоваться concurrent
collection.
Конечно, возможно предположить, что об этом факте кто-то может не знать, но
рассуждения автора о фактах приводят к мысли, что у него самого в голове
творится "каша". Это выглядит как диалог (действующие лица: project-manager компании
Goyello, разработчики):
ПМ:
Коллекции в языке C#
не потокобезопасны!!!
Разработчики:
Да, Кэп.
ПМ:
Ошибка, которая возникла не в основном потоке, не будет отловлена в основном
потоке.
Разработчики:
Да, капитан.
ПМ:
Если я выйду с программы, блок finally не
выполнится.
Разработчики:
Да, капитан.
У меня вопрос к разработчикам, которые читали данную статью: вы действительно видите какую-то полезность в этих очевидных советах? Или это только я отнесся к ним с придирками?L
Your program’s
instructions can be reordered by compiler, CLR or CPU to improve efficiency
Не
могу не согласиться с последним фактом. Хотя автор и не приводит достаточного
описания, почему так может произойти, это, пожалуй, единственный интересный
факт, который сложно назвать обыденным. Кеширование в CLR не
всегда происходит сразу. Для читателей своего блога я рекомендовал бы ознакомиться
с темой: "Учимся писать правильные C#-бенчмарки" для понимания принципа работы кеширования. Этот факт нельзя
поддать критике, как предыдущие заметки.
Итоги
В приведенной статье Вы ознакомились с субъективной
критикой статьи Threading in C#: 7 Things you should always remember about.
Возможно, в некоторых моментах мое мнение было жестковатым. Мне понравился
комментарий MVP Сергея Теплякова об одной из статей этого автора:
"Мне не хочется даже
обсуждать этот совет, поскольку в нем нет никакого рационального звена. Но меня
весьма пугает судьба компании Goyello, чей project manager раздает подобные
советы". Было бы лучше, если бы автор оригинальной
статьи описал преимущества в некоторых случаях ThreadPool против Thread, привел различие между обычным потоком и background-потоком, проблемы многопоточного доступа к
некоторым данным, описал бы о concurrent collection. Это все принесло бы больше пользы, чем пост о
семи вещах, из которых только пару (после грандиозных правок) можно назвать актуальными. Цель этой статьи - заставить
разработчика думать, прежде чем воспринимать какие-то факты из разных
источников. В том числе, нужно воспринимать также со скептицизмом факты,
которые описаны здесь. Главное для разработчика - думать, перед тем как использовать непроверенные
факты на практике. Тогда у Вас не возникнет никаких проблем с потоками.
No comments:
Post a Comment