Я недавно закончил последний класс моей программы магистра наук в области компьютерных наук в Университете Франклина . Я должен был написать небольшую статью для этого класса, которой, я думаю, стоит поделиться с вами. Статья была написана с аудиторией, поэтому она немного проще и менее детализирована, чем должна быть. Тем не менее, я думаю, что это имеет некоторое преимущество для программистов, которые пытаются начать тестирование своего параллельного кода. Теперь, когда я закончил со школой, я надеюсь изучить эту тему более подробно, ведя блог полностью. Вот содержимое статьи.
Аннотация
За время существования языка Java разработчики Java превратили свой жизненный цикл разработки в сложную и сложную экосистему инструментов, практик и соглашений. Вместо того, чтобы полагаться на интуицию и устные соглашения о том, что программное обеспечение было протестировано просто «достаточно хорошо» для запуска в производство, современный Java-разработчик полагается на показатели скорости прохождения многочисленных типов тестов и качества кода, прежде чем решить, что код готов к работе. , К сожалению, это относится только к последовательному коду Java, а не к параллельному коду. Несмотря на наличие более сложных показателей, чем последовательный код, нет оправдания для исключения многопоточного кода Java из этой современной среды разработки. Этот документ поможет вам собрать инструменты и методы, необходимые для модернизации способов разработки параллельного кода Java.
Подтверждения
Автор хотел бы поблагодарить Venkat Subramaniam и Daniel Hinojosa за тщательный обзор этой статьи и их содержательные комментарии.
Вступление
— Проблема в том, что программисты не так боятся использовать потоки, как следовало бы.
Дэвид Ховермейер и Уильям Пью
Создание программного обеспечения, которое может быть запущено несколькими потоками одновременно, является непростой задачей, которая затмевается только самим тестированием этого кода. Тем не менее, параллельный код может быть проверен. Тем не менее, чтобы перейти к теме тестирования, сначала нужно изложить некоторые основы. Существуют некоторые контекстуальные соображения, которые необходимо учитывать, прежде чем можно будет с пользой тестировать параллельный код. В этом документе будут рассмотрены эти фундаментальные проблемы и способы их решения в целях ознакомления с тем, как начать тестирование параллельного кода Java.
Современная модель разработки Java
Не существует стандартного способа разработки программ на Java. Тем не менее, разговоры между разработчиками Java, выступления консультантов и докладчиков на конференциях, а также выступления блогеров и профессиональных авторов отражают общепринятые на сегодняшний день практики Java-программистов. Из этого общего знания вы можете понять, какие основные инструменты и процессы используются современным Java-разработчиком.
Современный Java-разработчик выполняет всю свою разработку из интегрированной среды разработки (IDE), такой как Eclipse, NetBeans или IntelliJ IDEA. В IDE они могут выполнять быстрое и частое рефакторинг (Fowler) своего кода, который включается модульными тестами, которые постоянно проверяют правильность изменений. Современный Java-разработчик разбирается в модульном тестировании и, независимо от того, соблюдает ли он предписания Test Driven Development (Beck) или нет, тратит много времени на написание этих тестов. Когда код передается в управление исходным кодом, более автоматизированная часть процесса начинается, когда какая-либо форма системы непрерывной интеграции (CI) (Duvall и др.) Выбирает новый код. Полная компиляция и запуск всего пакета модульных тестов выполняется системой CI, как минимум, для постоянной проверки кода. Если они существуют,Для обеспечения дальнейшей проверки можно запустить более сложные автоматизированные интеграционные, функциональные, регрессионные или приемочные тесты.
Помимо компиляции и тестирования, современный Java-разработчик опирается на метрики. Во время сборки CI обычно запускается набор инструментов для кода, который собирает метрики о его работоспособности. Количество строк в исходном коде, различные инструменты статического анализа и инструменты покрытия кода являются общими. Разработчик понимает, как интерпретировать выходные данные этих инструментов, чтобы их выводы могли быть использованы для улучшения кода. Улучшения осуществляются путем внесения изменений; от простого добавления отсутствующих модульных тестов до исправления скрытых ошибок.
В этом наборе инструментов и практик подразумевается, что рассматривается только последовательный код Java. Как работать с многопоточным кодом в этой современной среде разработки Java никогда не обсуждали. Оставшаяся часть этой статьи поможет вам начать находить, тестировать и проверять многопоточный код таким образом, чтобы он соответствовал вашей роли современного разработчика Java.
Поиск ошибок параллелизма
Вы не можете проверить код, если вы не знаете о его существовании. Поэтому прежде чем вы сможете приступить к разработке тестов для своего многопоточного кода, вы должны найти его в своей кодовой базе. Если вы пишете тесты во время написания этого кода, вам не нужен этот раздел. Однако, если вы хотите протестировать существующий код, вы можете воспользоваться приведенными здесь советами, чтобы помочь вам отточить код, который вы, возможно, захотите проверить на многопотоковую корректность.
Поиск многопоточного и связанного кода
Одна из причин того, что работа с многопоточным кодом Java настолько сложна, заключается в том, что нетрудно выяснить, с каким кодом может взаимодействовать множество потоков во время выполнения. Начните думать об этой проблеме, задав себе простой вопрос «знаю ли я, где мой многопоточный код находится в моей кодовой базе?» — очень вероятно, что вы не сможете точно ответить на этот вопрос. Хотя разумной идеей является сохранение многопоточного кода в изоляции, вы можете обнаружить, что ваш многопоточный код разбросан по всей базе кода. Есть два простых способа получить лучшее представление о том, где находится этот код: выполнить несколько простых текстовых поисков и поработать с вашими коллегами.
В вашем распоряжении есть несколько простых, но быстрых и эффективных инструментов для поиска многопоточного кода. IDE современного разработчика Java является наиболее удобным в использовании. Тем не менее, некоторые простые инструменты командной строки могут сделать то же самое. В обоих случаях ответственность за определение кода, который может стать причиной сбоя при одновременном запуске в нескольких потоках, лежит на разработчике.
В вашей IDE нет инструмента, который бы перечислял вам весь исходный код, который мог бы выполняться несколькими потоками, поэтому вам нужно найти способ найти этот код. В листинге 1 приведен набор текстовых строк, по которым можно искать, что дает хорошее начало для поиска многопоточного кода. Все строки в листинге раскрывают код, который определяет поток, вызывает методы объекта Thread или использует низкоуровневые механизмы блокировки в API параллелизма Java.
"implements Runnable", "extends Thread", "synchronized", ".notify()", ".notifyAll()", ".wait()", ".wait(...)", ".interrupt()", ".interrupted()", ".join()", ".join(...)", ".sleep(...)", ".yield()", "import java.util.concurrent.Atomic", "import java.util.concurrent.locks", “InterruptedException”
Листинг 1: Текстовый поиск, помогающий обнаружить параллельный код.
Выполнение простого текстового поиска этих строк в вашей IDE покажет большую часть кода, который вы ищете [1] . Если вы предпочитаете командную строку, вы можете использовать команду поиска Unix, как показано в листинге 2, чтобы выполнить подобный поиск.
find . -name *.java -exec grep -n -H --color "implements Runnable" {} \;
Листинг 2: Использование команды поиска Unix для поиска параллельного кода.
Однако эти поиски могут принести только так много. Любой класс Java может быть создан и использован потоком, который не всегда легко идентифицировать. Например, в листинге 3 показано, как любой класс Java может использоваться в потоке. Найдя класс SimpleThread из приведенного выше поиска, вы должны изучить этот класс, чтобы определить, какие классы он использует, поскольку эти ссылочные классы теперь подвержены многопоточности.
public class SimpleThread implements Runnable { private JavaClass jc; public void run() { jc = new JavaClass(); } }
Листинг 3: Типичный класс Java, на который ссылаются из потока.
Простой текстовый поиск — это простой способ определить, какой код может быть проверен, но он не предоставляет никакой информации о природе и предназначении кода. Например, программистам может быть известен некоторый код, который не нуждается в безопасности потоков. Для других частей кода может быть совершенно неожиданным, что он может выполняться несколькими потоками. Учитывая это, было бы неплохо реализовать процесс рецензирования, чтобы проанализировать серьезность необходимости проверки найденного кода на многопотоковую безопасность (Гетц). Когда опытная часть команды проанализирует код, многие важные контекстуальные детали будут удалены, которые не очевидны ни одному программисту. Результаты экспертной оценки могут быть использованы для определения стратегии тестирования.
Использование статического анализа для выявления ошибок параллелизма
Понимание того, где параллелизм существует в вашем коде, является необходимым предварительным условием для тестирования этого кода. Однако простое нахождение кода не выявляет в нем ошибок. Поэтому вам нужно перейти от поиска параллельного кода к поиску ошибок в нем, а затем к тестированию, чтобы убедиться, что он не содержит ошибок — и все это в соответствии с вашей современной средой разработки. Инструменты статического анализа предлагают способ достижения среднего уровня путем выявления некоторых ошибок в вашем параллельном коде. В этом разделе будет обсуждаться инструмент статического анализа с открытым исходным кодом под названием FindBugs (Hovemeyer & Pugh).
FindBugs — это надежный инструмент для выбора всевозможных «шаблонов ошибок» в коде Java. FindBugs использует статический анализ и эвристику для поиска шаблонов кода и использования API, которые указывают или, как известно, вызывают ошибки (Hovemeyer & Pugh). Инструмент достаточно эффективен и в достаточной степени избавлен от ложных срабатываний, поэтому его использовали для поиска ошибок в рабочем коде в Google (Ayewah и др.). Однако более уместным для этого обсуждения является тот факт, что FindBugs может находить ошибки в параллельном коде. Фактически, FindBugs — единственный инструмент статического анализа, предложенный Брайаном Гетцем для использования с многопоточным кодом в главе «Тестирование параллельных программ» в (Goetz). В той же главе содержится краткое описание шаблонов одновременных ошибок, которые FindBugs может идентифицировать, включая несогласованную синхронизацию, невыпущенные блокировки,уведомления об ошибках и спин-петли.
Глава Гетца о параллельном тестировании устарела в одном вопросе. Он оценивает такие инструменты, как FindBugs, как «достаточно эффективные, чтобы быть ценным дополнением к процессу тестирования», но предупреждает, что они «все еще несколько примитивны (особенно в их интеграции с инструментами разработки и жизненным циклом)». Гетц ссылается на тот факт, что в раннем детстве FindBugs был доступен только как неуклюжее приложение Java Swing. Это уже не так, поскольку FindBugs теперь обычно интегрируется в набор инструментов современного разработчика Java. В качестве основы есть задача Ant для FindBugs, которую можно настроить для вывода XML. Далее, несколько инструментов CI, таких как Hudson , Jenkins , Bamboo и Sonarесть плагины, которые могут создавать сложные панели мониторинга из вывода FindBugs XML. Задача Ant позволяет автоматически запускать анализ FindBugs как часть процесса сборки, где вывод XML может быть получен плагином CI для создания панели мониторинга. Панели мониторинга включают в себя все результаты FindBugs, но упрощают детализацию только ошибок параллелизма.
Рисунок 1: Сводная страница FindBugs в Гудзоне.
FindBugs — это второй шаг в цепочке инструментов тестирования параллелизма, главным образом потому, что он очень прост в использовании. Чтобы использовать задачу Ant, вы просто указываете FindBugs, где найти скомпилированные файлы .class, где найти исходный код и где записать выходной файл. После создания вывода XML для подключаемых модулей CI для FindBugs в качестве входных данных обычно требуется только путь к этому файлу. Плата за такие простые шаги огромна. На рисунке 1 показан вид плагина панели поиска FindBugs для CI-сервера Hudson, в котором указано количество ошибок, обнаруженных в категории «многопоточная корректность» [2] . На рисунке 2 показана способность инструментальной панели точно определять строки кода, содержащие ошибки. В этом случае класс SubmitBrochureOrder содержит три ошибки параллелизма (изменяемые поля сервлета).
Рисунок 2: Панель инструментов FindBugs, показывающая ошибки в классе SubmitBrochureOrder.
Измерение количества одновременно тестируемого кода
До настоящего времени эта статья охватывала два процесса: определение того, какой параллельный код существует в вашей кодовой базе, и определение того, содержит ли ваш код какие-либо распространенные шаблоны ошибок параллелизма. Еще одним предшествующим фактическим тестированием кода является определение того, сколько параллельного кода действительно тестируется с точки зрения безопасности потоков. Современные Java-разработчики привыкли измерять «покрытие кода» (Miller & Maloney) и почти повсеместно проводят как блок модульных тестов, так и анализ покрытия кода как фундаментальную часть своего процесса сборки. Обычно даже основывают работоспособность кодовой базы частично на объеме покрытия кода. Эта практика хорошо известна (Гловер), но это все еще эффективный способ понять, уделяется ли внимание модульному тестированию кода или нет.
Вряд ли есть аргумент против полезности анализа покрытия кода, но только для последовательного кода. Типичные инструменты анализа покрытия кода, используемые современными разработчиками Java, такими как Cobertura и Emma , не предназначены для измерения того, насколько хорошо параллельные аспекты кода охватываются юнит-тестами. Фактически, в настоящее время очень мало используется модульного тестирования для параллелизма, хотя Гетц вкратце объясняет, как параллельный код в API-интерфейсах Java подвергается модульному тестированию (Goetz).
Чтобы решить эту проблему, команда исследователей из IBM разработала теорию и инструменты для измерения степени, в которой параллельный код тестируется на безопасность потоков, и окрестила его «охватом синхронизации». Эта концепция не аналогична метрике покрытия кода для последовательного кода. Принимая во внимание, что покрытие последовательного кода измеряет процент строк исходного кода, ветвей, методов и классов, которые выполняются во время тестирования, покрытие синхронизации измеряет процент критических разделов многопоточного кода, которые выполняются более чем одним потоком одновременно во время выполнения теста. Покрытие синхронизации — это общий термин для нескольких «задач покрытия», каждая из которых измеряет различные аспекты тщательности параллельного тестирования. В качестве примера задачи покрытия, если во время выполнения тестовсинхронизированный блок кода не доступен нескольким потокам одновременно, этот блок кода не считается покрывающим. И наоборот, если один или несколько потоков должны бороться за блокировку в этом критическом разделе кода, код считается покрытым (Bron, et. Al.).
Эта метрика может быть довольно показательной, особенно если вы только начинаете добавлять тесты в существующую кодовую базу. Получение покрытия синхронизации вашей кодовой базы немедленно скажет вам, проверяется ли потокобезопасность вашего кода вообще вашим существующим набором тестов. Поскольку теперь вы знаете, где находится ваш параллельный код, и в нем уже устранены легко обнаруживаемые ошибки параллелизма, вы можете использовать покрытие синхронизации для планирования того, что на самом деле должно начинаться тестирование. Следующий раздел посвящен тестированию параллельного кода, который включает использование инструмента IBM ConTest, который реализует метрику покрытия синхронизации.
Тестирование параллельного кода
Как и в случае с тестированием в целом, существует множество методов для тестирования параллельного кода (ватт). Этот раздел будет посвящен двум конкретным способам тестирования, которые особенно хорошо вписываются в портфель инструментов современного разработчика Java. Две формы тестирования — это модульное тестирование и автоматическое поисковое тестирование. Оба типа могут использоваться для тестирования многопоточного кода, чтобы гарантировать его правильную работу при одновременном запуске в нескольких потоках.
There are few options for unit testing concurrent code. However, one promising option is the MultithreadedTC library(Pugh & Ayewah). This library uses the novel abstraction of a “metronome tick” to provide a mechanism for sequencing the interleaving of multiple threads. The library automatically moves to the next “tick” every time all the threads in a running MultithreadedTC unit test are blocked. The tester can then assert that various conditions are held for a specific tick. The metronome tick allows MultithreadedTC unit tests to verify the correctness of code’s multithreaded behavior in a way that does not interfere with the natural scheduling of threads by the JVM.
MutlithreadedTC is promising for two reasons. First, it is highly automatable, which fits perfectly into the modern Java developer’s environment. MultithreadedTC is built on JUnit, which makes it easy to run in an Ant script as part of the CI build. This also means that it produces JUnit reports that can be viewed on the JUnit dashboard of a CI server. Second, MultithreadedTC is a rare tool in that it gives the developer the control to test very specific threading scenarios. Any number of threads can be used in a test and the metronome tick allows the finest granularity of testing possible. This is a powerful and unique technique that does not exist in many other multithreaded testing tools.
In contrast, IBM ConTest is a no less useful and important tool that takes control away from the tester (Edelstein, et. al.). Rather than asking the tester to tediously code fine-grained threading scenarios, ConTest randomly explores as many of the thread interleavings as possible. The tester gets to write tests that more closely resemble serial unit tests, which ConTest then runs across a varying number of multiple threads simultaneously over a period of time. The idea behind ConTest is to help reduce the complexity of concurrency testing by covering as many threading scenarios as possible. In doing so, the chance of finding a concurrency bug increases.
ConTest is also highly automatable. It produces reports on the number of test passes and failures and even includes metrics on synchronization coverage. Since it simply instruments your code, the developer can use any tests written for that code when running ConTest. In most modern Java development CI systems, this amounts to adding two extra steps to the build process.
Conclusion
Modern Java developers are comfortable with the cycle of developing code and tests simultaneously, building the code using a slew of tools for automation and then automatically verifying the code and producing metrics on the health of their code. Metrics and verification are used as a means to constantly improve the quality of the code. Even now, however, that entire process is focused on serial code only. It has been shown in this paper that developing tests for and verifying the quality of concurrent code can be integrated into this complex development environment as smoothly as the tools for serial code.
Future Work
It has been shown that there are more effective algorithms for finding bugs than what FindBugs and IBM ConTest can provide (Eytani, et. al.). However, these algorithms are not yet embedded in tools that are useful for developers outside of academia. Some advanced industry tools (Dern & Tan) do make use of some of these algorithms, but there is little movement in this area in the Java ecosystem. Further, synchronization coverage is a straightforward and useful tool that could be integrated with serial code coverage seamlessly. There is, however, no tool known to the author other than IBM ConTest that implements synchronization coverage metrics for Java code. It should be feasible, and is certainly desirable, that synchronization coverage be an additional part of existing code coverage analysis tools like Cobertura and Emma.
Bibliography
Ayewah, N., et. al. (2007). “Using FindBugs on Production Software”. OOPSLA’07, October 21–25. Montréal, Québec, Canada.
Beck, K. (2003). Test-Driven Development by Example. Upper Saddle River, NJ: Addison Wesley.
Bron, A., et. al. (2005). “Applications of Synchronization Coverage”. PPoPP’05, June 15–17. Chicago, Illinois, USA.
Dern, C., Tan, R. (2009). “Code Coverage for Concurrency”. MSDN Magazine from http://msdn.microsoft.com/en-us/magazine/ee412257.aspx.
Duvall, P., Matyas, S., Glover, A. (2007). Continuous Integration: Improving Software Quality and Reducing Risk. Upper Saddle River, NJ: Addison Wesley.
Edelstein, O., et. al. (2008). Automating the Testing of Multi-threaded Java Programs. IBM Research Laboratory in Haifa
Eytani, Y. et. al. (2006). “Towards a framework and a benchmark for testing tools for multi-threaded programs”. Concurrency Computat.: Pract. Exper. 2007; 19:267–279.
Fowler, M. (1999). Refactoring Improving the Design of Existing Code. Upper Saddle River, NJ: Addison-Wesley Professional.
Glover, A. (January, 2006). “In pursuit of code quality: Don’t be fooled by the coverage report”. IBM DeveloperWorks from http://www.ibm.com/developerworks/java/library/j-cq01316/.
Goetz, B., et. al. (2006). Java Concurrency in Practice. Upper Saddle River, NJ: Addison-Wesley.
Hovemeyer, D., Pugh, W. (2004). “Finding Bugs is Easy”. OOPSLA’04, Oct. 2428. Vancouver, British Columbia, Canada.
Miller, J., Maloney, C. (February 1963). “Systematic mistake analysis of digital computer programs”. Communications of the ACM. New York, NY, USA.
Pugh, W., Ayewah, N. (2007). Unit testing concurrent software. IEEE/ACM International Conference on Automated Software Engineering, Atlanta, GA, USA.
Watts, N. (March, 2011). “A Survey of Methods and Tools for Testing Parallel and Concurrent Programs”. Written for Comp 674 at Franklin University.
[1] These strings will only reveal the most obvious multi-threaded code. There are other much trickier situations which have been left out of the scope of this paper (Goetz).
[2] The dashboard has been created from the FindBugs output for a real production code base developed at Ohio Mutual Insurance Group.
From http://thewonggei.wordpress.com/2011/07/18/getting-started-testing-concurrent-java-code/