Thursday, January 30, 2014

Как разработчики C# видят использование потоков в своих программах

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

Программистов, работающих с потоками, можно разделить на три группы. К первой группе принадлежат разработчики, которые максимально избегают потоков, даже в тех случаях, где они необходимы. Возникает вопрос: почему так происходит? По моим предположениям, причина избегания потоков кроется в дороговизне операции создания потоков, и использование их в программе сопряжено с рисками. Оказывается, часть моих рассуждений была верна, но на самом деле ситуацию определяет то, что среди таких разработчиков бытует мнение, что использование в программе потоков - плохой стиль. Другие прогаммисты стараются построить всю работу вокруг Asynchronous Programming Model (APM). Такие специалисты избегают использования тасков в своих программах, потому что работа с тасками требует в некоторой мере перестройки мышления: начать думать не объектами потоков, а объектами тасков. Как только таким разработчикам доводится писать код под, например, .NET 4.5, где таски почти всюду, их это угнетает. Когда иссякает терпение насчет моделей тасков, эти разработчики пишут свои врапперы, которые строятся над тасками, чтобы построить все на синхронном режиме. Интересный факт, что возраст таких разработчиков колеблется от 30 до 35 лет, и они уверены в своем превосходстве, и поэтому всячески избегают "новомодных фич" конкретного языка разработки. Они считают, что все нововведения в языке - это ерунда, так как это можно решить старыми добрыми проверенными способами. С такими программистами спорить бессмысленно, они не воспринимают чужого мнения, считая свое мнение единственным правильным.

Вторая категория разработчиков старается использовать потоки и там, где они действительно нужны, и там, где от них будет вред. Обычно это разработчики-джуниоры, которые только что прочитали о потоках, например, у Троелсена, посмотрели возможности библиотеки Task Parallel Library (TPL) и начинают их использовать где попало, зачастую неправильно. У таких разработчиков есть большая проблема: они зачастую попадают в команду разработки с первой и третьей категорией разработчиков. Если их руководитель попадет к разработчикам с первой категории, то у начинающего программиста отобьют все желание использовать потоки, при этом основная аргументация будет в виде: "это плохо". Со временем это приведет к тому, что сформируется очередной программист, который относится к первой группе. Если руководителем такого начинающего разработчика будет опытный наставник, то большая вероятность, что данный разработчик научится правильно и уместно использовать потоки. Есть еще третий путь, который приходится проходить молодым разработчикам: самостоятельный путь проб и ошибок. Обычно это подразумевает получение глубокой теоретической базы и умение применять эту базу хоть изредка на практике. Человек имеет все необходимые навыки для саморазвития. Большинство специалистов проходят путь самоучек, так как в современном мире работать в команде со специалистами, которые умеют формулировать свои мысли и знают много тонкостей языка, - это достаточно редкое явление. Такие специалисты обычно много "просят" за свой труд, так как таких "гуру" зачастую нанимают для самых сложных задач, с которыми не справляются другие разработчики. Если Вы начинающий разработчик, пожалуй, единственный совет, который могу Вам дать, - это читать побольше литературы и смотреть, что пишут о потоках такие профессионалы, как Эрик Липперт, Джон Скит, Джеффри Рихтер и другие. Вы можете положиться только на самых себя. Вам будут очень редко помогать другие разработчики, которые знают больше, потому что чаще всего такие разработчики будут Вас высмеивать и стараться показывать свое превосходство. Этот факт я заметил на многих русскоязычных форумах по разработке ПО, а также в компаниях, которые разрабатывают такое программное обеспечение и штат разработчиков которых состоит из русскоязычных программистов. Вероятнее всего, это связано с менталитетом нашего народа.

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

Итоги

Бывает сложно общаться с такими специалистами, которые при слове "многопоточность" начинают нести ахинею. У таких людей нет понятия золотой середины, они переходят из одной крайности в другую. Посмотрите в интернете посты по убожеству тасков, по сравнению Reactive Extensions, или наоборот. Или когда начинают оптимизировать выполнения потока, которое иногда доходит к крайности. Почему таким разработчикам сложно комбинировать возможности, например Reactive Extensions и тасков, если это позволяет задача, почему не использовать возможности многопроцессорности ПК для решения математической задачи или такой задачи, которая требует множества времени на выполнение на основании тасков. Это сэкономит время и ресурсы системы. Правильная архитектура системы должна обеспечиваться комбинацией возможных технологий и решений, стараясь соблюдать гармонию между крайностями в использовании инструментов и возможностей для разработки многопоточного приложения. Просто перед тем, как принимать решение, стоит подумать: возможно, решение, предложенное коллегой, не является неправильным. Оно зачастую не совпадет с Вашим решением, но это не значит, что оно неверное. Как разработчики и думающие люди, давайте подходить к решению задач коллективными решениями. 

Wednesday, January 29, 2014

6 вещей, которые (не)обязан делать C# разработчик

Предлагаю обсудить статью под названием "6 more things C# developers should (not) do". Этот материал, в отличие от статьи "Threadingin C#: 7 Things you should always remember about", не вызывает у разработчиков когнитивный диссонанс. Правда, меня немного смутил тот факт, что автор приводит ссылку на свою предыдущую статью "8 Most common mistakes C# developers make", которая содержит настолько поверхностно описанные, спорные моменты, что анализируемая статья сначала не вызвала особого энтузиазма (большее впечатление, чем сама статья, на меня произвела рецензия на нее: "8 наиболее распространенных ошибок C# программистов")
Очертим пункты статьи, которую нам предлагает Pawel Bejger, project manager компании Goyello.  

Try to avoid using the “ref” and “out” keywords.
В данном описании соглашусь с автором, что данный подход нарушает Single Responsibility Principle (Принцип Единой Ответственности), но у нас есть обратная сторона медали, которую автор называет "исключением из правил". Примером такого исключения служит изменение value type при передаче в метод функции и т.д. (часто такой подход наблюдается при использовании Win32 API структур данных). Использование для передачи без параметра ref приводит к копированию значения. Но в целом автор прав. Если в Вашем коде часто используется ref или out, то с большой вероятностью здесь проблема в реализации и классы пытаются выполнить код, который они не должны делать.
Do not use OrderBy before Where
В данном подходе я немного не согласен с аргументацией автора. Это здравый факт, что по производительности нецелесообразно сначала отсортировать данные, а затем для необходимых из них выполнить некое условие по выборке данных. К сожалению, мне ни разу не приходилось видеть тот факт, что вначале выполняется сортировка, а затем выборка. Предполагаю, автор статьи знаком с разработчиками, которые пишут такой код, поэтому он не счел данный факт очевидным.
Always use Properties instead of public variables
Соглашусь с автором по поводу того, что снаружи не должны быть доступны публичные переменные, но использование вместо них обычных автопроперти не решает ситуацию. Многие разработчики используют вместо публичных переменных публичные автопроперти.

class SomeClass
{
    public int _value;
    public int TestProperties { get; set; }
}

В данном подходе использование проперти вместо публичных переменных не особо спасает ситуацию. Рихтер говорит о таком явлении как о злоупотреблении автопроперти. Вариант с использованием переменных, которые доступны извне для изменений, обычно искореняется на основании принятого корпоративного стиля разработки. Если в компании такого стиля нет, то здесь бороться и утверждать, что такое хорошо, а что такое плохо, бессмысленно. Дешевле для такой компании предложить воспользоваться услугами компании JetBrains. А именно - ее продуктом, который называется ReSharper. Сложно в данном примере, приведенном автором, найти связь, которую он хотел провести между свойствами (properties) и переменными, так как это разные вещи. Даже в том контексте, который имел в виду автор, предложение звучит с ноткой бессмыслицы, потому как говорить в этом контексте только о свойствах, не упоминая при этом функций или методов, недостаточно.
Take advantage of string.IsNullOrEmpty() and string.IsNullOrWhiteSpace()
С этим пунктом также сложно поспорить. Ввиду своей очевидности этот подход можно назвать К. О.; он чем-то напоминает подход автора, замеченный в его статьях ранее. Возможно, буду не прав, но считаю, что компания может обеспечить своих разработчиков компонентом, чтобы автоматизировать этот процесс. Если компания не может выделить средств на покупку ReSharper, то в этом случае с помощью Roslyn API, доступным для скачивания и разработки и который войдет в язык C# как составная часть, Вы можете написать правило, которое позволит, не раздумывая о таких тонких нюансах, делать это автоматически.
Understand the difference between First() and Single()
Пожалуй, еще один очевидный факт. Но в последнее время часто наблюдается тенденция использования FirstOrDefault() и SingleOrDefault(), так как разработчикам лень обрамлять свои конструкции блоками try->catch.
Do not blindly use List
Автор рекомендует в некоторых случаях использовать HashSet или SortedHash как замену List для того, чтобы повысить производительность. Автор прав в данном случае по поводу альтернативы HashSet в некоторых случаях вместо List. Но тот пример, который привел автор для сравнений производительности, просто убил меня наповал.
static void Main(string[] args)
    {
        const int COUNT = 100000;
        HashSet<int> hashSetOfInts = new HashSet<int>();
        Stopwatch stopWatch = new Stopwatch();
        for (int i = 0; i < COUNT; i++)
        {
            hashSetOfInts.Add(i);
        }

        stopWatch.Start();
        for (int i = 0; i < COUNT; i++)
        {
            hashSetOfInts.Contains(i);
        }
        stopWatch.Stop();

        Console.WriteLine(stopWatch.Elapsed);

        stopWatch.Reset();
        List<int> listOfInts = new List<int>();
        for (int i = 0; i < COUNT; i++)
        {
            listOfInts.Add(i);
        }

        stopWatch.Start();
        for (int i = 0; i < COUNT; i++)
        {
            listOfInts.Contains(i);
        }
        stopWatch.Stop();

        Console.WriteLine(stopWatch.Elapsed);
        Console.ReadLine();
    }
}
Я не предполагал, что есть люди, которые сравнивают производительность коллекций, построенных на основании хешей, и коллекций по значению.  Как можно сравнивать такие вещи, ведь у данных коллекций разная эффективность.

Вместо непонятного сравнения линейной сложности коллекции List с HashSet, который имеет постоянную сложность, рекомендую ознакомиться с эффективностью использования коллекций в .NET "C#/.NETFundamentals: Choosing the Right Collection Class", с этого источника можно узнать намного больше, чем с исходной статьи.

Итоги
Анализируемая статья в автора написана на порядок лучше, чем его предыдущие статьи, но утверждения в ней выглядят из разряда К.О. Автор мог бы расширить свой список и добавить для расширения от банальности, например, пункты: "использование IEnumerable<> вместо явного использование типов List<>, Array и других коллекций"; "избегайте использования множества потоков", "не забываем using для классов, которые реализуют интерфейс IDisposable". А если разработчик пишет под .NET 2.0 или Compact Edition, то часть предложенных позиций ему просто не нужна. Интересно, почему из шести вещей, которые, по мнению автора, должен делать каждый разработчик, две из них посвящены linq. Надеюсь, статья открыла для вас очевидные вещи в языке C#, о которых Вы, могли не знать или использовать неверно J.  

  

Tuesday, January 28, 2014

Потоки в С#: 7 вещей, которые Вы обязаны знать

Недавно на работе нам прислали несколько “интересных ссылок по языку 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")может привести к проблеме, поскольку любой код в процессе, используя ту же строку, будет совместно использовать ту же блокировку.
Для блокировки рекомендуется определять объект private или переменную объекта private static, чтобы защитить данные, являющиеся общими для всех экземпляров.


Складывается впечатление, что автор неуклюже пытается донести то, что сказано в 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. Это все принесло бы больше пользы, чем пост о семи вещах, из которых только пару (после грандиозных правок) можно назвать актуальными. Цель этой статьи - заставить разработчика думать, прежде чем воспринимать какие-то факты из разных источников. В том числе, нужно воспринимать также со скептицизмом факты, которые описаны здесь. Главное для разработчика - думать, перед тем как использовать непроверенные факты на практике. Тогда у Вас не возникнет никаких проблем с потоками.