пятница, 24 февраля 2012 г.

MTA vs. STA, или история решения одной проблемы


Обращали ли вы когда-либо внимание на обязательный атрибут [STAThread], которым помечен практически любой метод Main(). Нет, он не расшифровывается, как “запустить поток” (прим. переводчика: в оригинале «start thread»). Он определяет что метод будет выполнятся в однопоточном апартменте. Недавно, этот атрибут был причиной серьезной проблемы, с которой мне довелось столкнуться.
У нас есть несколько программ, которые считывают из базы данных гигантские объемы информации, осуществляют некоторые преобразования над этими данными и экспортируют результат в Excel. Эти программы работали невообразимо медленно. Невообразимо медленно не  в том плане, что имеют дело с огромным количеством данных, а в том, что при увеличении объема обрабатываемых данных, продолжительность процесса обработки увеличивается экспоненциально. Например, при увеличении базы данных на 10% продолжительность процесса обработки данных увеличивается с одного часа до трех с половиной часов.
Пытаясь разобраться в проблеме, я первым делом начал подозревать наш код, тем более что и Task Manager указывал на то, что объем используемой памяти увеличивался, что приводило к большему использованию процессора (существенно большему использованию). Я приступил к поиску утечек памяти. Поскольку программа имела дело с большими объемами памяти, у нас был реализован механизм чтения данных небольшими порциями, далее осуществлялась обработка прочитанного и в конце концов использованный фрагмент памяти отдавался (или как мы думали) на съедение сборщику мусора. Далее мы считывали следующую порцию данных и процесс повторялся заново. Подобным образом велась работа и со StreamWriter, который не держал весь выходной файл в памяти перед записью, а также обрабатывал данные сегментированно, каждую порцию отдельно.
Код, который занимался считыванием данных был хорошо протестирован, он часто использовался, и раньше мы никогда не имели с ним проблем. В нынешней же проблеме я начал подозревать StreamWriter. Возможно здесь мы имели дело с каким то буфером в памяти, который не сбрасывался при записи в файл. Я попробовал уничтожать и пересоздавать заново экземпляр StreamWriter для каждого нового фрагмента данных, в надежде что это решение поможет избавиться от нашей проблемы. Но оно не сработало.
Тогда я начал подозревать, что виновником проблемы может быть наш слой данных – или если быть более точным, объект DBConnection который обертывал SqlConnection. В нашей организации, слой данных – это наиболее важный, наиболее используемый и, соответственно, наиболее чувствительный элемент системы. И наличие в нем утечки памяти было бы просто непростительным. Однако, я проверил все что мог, и так и не смог найти ничего, что могло бы быть причиной нашей проблемы.
После непродолжительных поисков в интернете я обнаружил что и другие разработчики также сталкивались с подобной проблемой. Оказывается, при определенных обстоятельствах существует утечка памяти (если быть более точным, утечка хэндлов). Одним из обстоятельств, является использование SqlConnection в однопоточном апартменте. Изучая вопрос далее я нашел несколько решений проблемы, одним из которых было изменение типа апартмента с однопоточного на многопоточный.
К счастью, это реализуется простой заменой [STAThread] на [MTAThread]. Магическим образом, изменив всего одну букву в файле, я полностью решил свою проблему, и после этого процесс занимавший три с половиной часа стал выполнятся за 20 минут. Использование памяти и процессора стало стабильным и не увеличивалось в процессе работы программы.
Я выяснил, что с нашей логикой и с нашими библиотеками, которые мы использовали было все в порядке. Проблема была только в отсутствии понимания STAThread и MTAThread (о последнем до недавнего времени я даже и не слышал). Документация Visual Studio многого мне не рассказала, там лишь отмечено, что эти атрибуты имеет смысл использовать только когда необходимо работать с COM. Таким образом стало ясно, что SqlConnection в своих недрах должен использовать COM и все что мне нужно знать – это то что атрибут MTAThread волшебным образом решает мою ужасную проблему.
Позже, я провел небольшое исследование касательно типов апартментов и выяснил, что однопоточные апартменты имеют определенный механизм встроенной поточной сериализации, которая призвана защитить разработчика от себя самого. Это и послужило причиной нашей проблемы. Ну а в будущем, я пожалуй буду использовать только MTAThread – особенно когда предполагается использование SqlConnection.