Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
Рекомендация: реализуйте свой многопоточный код так, чтобы он мог выпол- няться в различных конфигурациях. Обеспечьте логическую изоляцию конфигураций многопоточного кода Правильный баланс программных потоков обычно определяется методом проб и ошибок . Прежде всего найдите средства измерения производительности си- стемы в разных конфигурациях . Реализуйте систему так, чтобы количество программных потоков могло легко изменяться . Подумайте, нельзя ли разрешить его изменение во время работы системы . Рассмотрите возможность автомати- ческой настройки в зависимости от текущей производительности и загрузки системы . 219 220 Глава 13 . Многопоточность Протестируйте программу с количеством потоков, превышающим количество процессоров При переключении контекста системы между задачами могут происходить вся- кие неожиданности . Чтобы форсировать переключение задач, выполняйте свой код с количеством потоков, превышающим количество физических процессоров или ядер . Чем чаще происходит переключение задач, тем больше вероятность выяв ления пропущенной критической секции или возникновения взаимной блокировки . Протестируйте программу на разных платформах В середине 2007-го года мы разрабатывали учебный курс по многопоточному программированию . Разработка курса велась в OS X . Материал курса излагался в системе Windows XP, запущенной на виртуальной машине . Однако сбои в те- стах, написанных для демонстрации ошибок, происходили в среде XP заметно реже, чем при запуске в OS X . Тестируемый код всегда был заведомо некорректным . Эта история лишний раз доказывает, что в разных операционных системах используются разные поли- тики многопоточности, влияющие на выполнение кода . Многопоточный код по- разному работает в разных средах 1 Протестируйте систему во всех средах, которые могут использоваться для ее развертывания . Рекомендация: многопоточный код необходимо тестировать на всех целевых платформах — часто и начиная с ранней стадии. Применяйте инструментовку кода для повышения вероятности сбоев Ошибки в многопоточном коде обычно хорошо скрыты от наших глаз . Просты- ми тестами они не выявляются . Такие ошибки могут проявляться с периодично- стью в несколько часов, дней или недель! Почему же многопоточные ошибки возникают так редко и непредсказуемо, по- чему их так трудно воспроизвести? Потому что лишь несколько из тысяч возмож- ных путей выполнения кода плохо написанной секции приводят к фактическому отказу . Таким образом, вероятность выбора сбойного пути ничтожно мала . Это обстоятельство серьезно усложняет выявление ошибок и отладку . 1 А вы знаете, что потоковая модель Java не гарантирует вытесняющей многопоточности? В большинстве современных ОС поддерживается вытесняющая многопоточность, которую вы фактически получаете автоматически . И все же JVM ее не гарантирует . 220 Тестирование многопоточного кода 221 Как повысить вероятность выявления таких редких ошибок? Внесите в свой год соответствующие изменения и заставьте его выполняться по разным путям — включите в него вызовы таких методов, как Object.wait() , Object.sleep() , Object. yield() и Object.priority() Каждый из этих методов влияет на порядок выполнения программы, повышая шансы на выявление сбоя . Сбои в дефектном коде должны выявляться как можно раньше и как можно чаще . Существует два способа инструментовки кода: Ручная . Автоматическая . Ручная инструментовка Разработчик вставляет вызовы wait() , sleep() , yield() и priority() в свой код вручную . Такой вариант отлично подходит для тестирования особенно коварных фрагментов кода . Пример: public synchronized String nextUrlOrNull() { if(hasNext()) { String url = urlGenerator.next(); Thread.yield(); // Вставлено для тестирования updateHasNext(); return url; } return null; } Добавленный вызов yield() изменяет путь выполнения кода . В результате в про- грамме может произойти сбой там, где раньше его не было . Если работа програм- мы действительно нарушается, то это произошло не из-за того, что вы добавили вызов yield() 1 . Просто ваш код содержал скрытые ошибки, а в результате вызова yield() они стали очевидными . Ручная инструментовка имеет много недостатков: Разработчик должен каким-то образом найти подходящие места для вставки вызовов . Как узнать, где и какой именно вызов следует вставить? Если вставленные вызовы останутся в окончательной версии кода, это при- ведет к замедлению его работы . Вам приходится действовать «наобум»: вы либо находите скрытые дефекты, либо не находите их . Вообще говоря, шансы не в вашу пользу . 1 Строго говоря, это не совсем так . Поскольку JVM не гарантирует вытесняющей много- поточности, конкретный алгоритм может всегда работать в ОС, не поддерживающей вы- теснения . Обратное тоже возможно, но по другим причинам . 221 222 Глава 13 . Многопоточность Отладочные вызовы должны присутствовать только на стадии тестирования, но не в окончательной версии кода . Кроме того, вам понадобятся средства для про- стого переключения конфигураций между запусками, повышающего вероятность обнаружения ошибок в общей кодовой базе . Конечно, разделение системы на POJO-объекты, ничего не знающие о многопо- точности, и классы, управляющие многопоточностью, упрощает поиск подходя- щих мест для инструментовки кода . Кроме того, такое разделение позволит нам создать целый набор «испытательных пакетов», активизирующих POJO-объекты с разными режимами вызова sleep , yield и т . д . Автоматизированная инструментовка Также возможна программная инструментовка кода с применением таких инстру- ментов, как Aspect-Oriented Framework, CGLIB или ASM . Допустим, в программу включается класс с единственным методом: public class ThreadJigglePoint { public static void jiggle() { } } Вызовы этого метода размещаются в разных позициях кода: public synchronized String nextUrlOrNull() { if(hasNext()) { ThreadJiglePoint.jiggle(); String url = urlGenerator.next(); ThreadJiglePoint.jiggle(); updateHasNext(); ThreadJiglePoint.jiggle(); return url; } return null; } Теперь в вашем распоряжении появился простой аспект, случайным образом вы- бирающий между обычным продолжением работы, приостановкой и передачей управления . Или представьте, что класс ThreadJigglePoint имеет две реализации . В первой реализации jiggle не делает ничего; эта реализация используется в окончатель- ной версии кода . Вторая реализация генерирует случайное число для выбора между приостановкой, передачей управления и обычным выполнением . Если теперь повторить тестирование тысячу раз со случайным выбором, возможно, вам удастся выявить некоторые дефекты . Даже если тестирование пройдет успешно, по крайней мере вы сможете сказать, что приложили должные усилия для выявления недостатков . Такой подход выглядит несколько упрощенно, но и он может оказаться разумной альтернативой для применения более сложных инструментов . 222 Заключение 223 Программа ConTest 1 , разработанная фирмой IBM, работает по аналогичному принципу, но предоставляет расширенные возможности . Впрочем, суть тестирования остается неизменной: вы ломаете предсказуемость пути выполнения, чтобы при разных запусках код проходил по разным путям . Комбинация хорошо написанных тестов и случайного выбора пути может ради- кально повысить вероятность поиска ошибок . Рекомендация: используйте стратегию случайного выбора пути выполнения для выявления ошибок. Заключение Правильно написать многопоточный код непросто . Даже очевидный, хорошо понятный код превращается в сущий кошмар, когда в игру вступают множествен- ные потоки и одновременный доступ к данным . Если вы столкнулись с задачей из области многопоточного программирования, вам придется приложить все усилия к написанию чистого кода или столкнуться с коварными, непредсказуе- мыми сбоями . Прежде всего следуйте принципу единой ответственности . Разбейте систему на POJO-объекты, отделяющие многопоточный код от кода, с потоками никак не связанного . Проследите за тем, чтобы при тестировании многопоточного кода тестировался только этот код, и ничего лишнего . Из этого следует, что мно гопоточный код должен быть компактным и сконцентрированным в одном месте . Знайте типичные источники многопоточных ошибок: работа с общими данными из нескольких программных потоков, использование пула общих ресурсов . Осо- бенно непростыми оказываются пограничные случаи: корректное завершение работы, завершение итераций циклов и т . д . Изучайте свои библиотеки и знайте фундаментальные алгоритмы . Разберитесь в том, как некоторые функции библиотек используются для решения проблем, сходных с проблемами фундаментальных алгоритмов . Научитесь находить секции кода, которые должны защищаться блокировками, и защищайте их . Не устанавливайте блокировки для тех секций, которые за- щищать не нужно . Избегайте вызовов одной заблокированной секции из другой заблокированной секции — для них необходимо глубокое понимание того, какие ресурсы находятся в общем или монопольном доступе . Сведите к минимуму количество совместно используемых объектов и масштаб общего доступа . Из- мените архитектуру объектов с общими данными так, чтобы они поддерживали одновременные обращения со стороны клиентов, вместо того чтобы заставлять самих клиентов заниматься управлением состоянием общего доступа . 1 http://www .alphaworks .ibm .com/tech/contest 223 224 Глава 13 . Многопоточность В ходе программирования неизбежно возникнут проблемы . Те из них, которые не проявляются на самой ранней стадии, часто списываются на случайности . Эти так называемые «несистематические» ошибки часто встречаются только при высокой нагрузке или вообще в случайные (на первый взгляд) моменты . Следовательно, вы должны позаботиться о том, чтобы ваш многопоточный код мог многократно запускаться в разных конфигурациях на многих платформах . Тестируемость, естественным образом проистекающая из трех законов TDD, подразумевает определенный уровень модульности, которая обеспечивает воз- можность выполнения кода в более широком диапазоне конфигураций . Потратив немного времени на инструментовку кода, вы значительно повысите шансы обнаружения некорректного кода . Инструментовка может производить- ся как вручную, так и с применением технологий автоматизации . Начинайте с ранних стадий работы над продуктом . Многопоточный код должен отработать в течение как можно большего времени, прежде чем он будет включен в оконча- тельную версию продукта . Если вы будете стремиться к чистоте своего кода, вероятность того, что вам удастся правильно реализовать его, значительно возрастет . литература [Lea99]: Concurrent Programming in Java: Design Principles and Patterns, 2d . ed ., Doug Lea, Prentice Hall, 1999 . [PPP]: Agile Software Development: Principles, Patterns, and Practices, Robert C . Martin, Prentice Hall, 2002 . [PRAG]: The Pragmatic Programmer, Andrew Hunt, Dave Thomas, Addison-Wesley, 2000 . 224 Последовательное очищение Дело о разборе аргументов командной строки В этой главе представлен вполне реальный сценарий последовательного очище- ния кода . Мы рассмотрим модуль, который внешне смотрелся вполне достойно, но плохо масштабировался . Вы увидите, как происходила переработка и очистка этого модуля . Многим из нас время от времени приходится заниматься разбором аргументов командной строки . Если под рукой не окажется удобного инструмента, мы просто перебираем элементы массива строк, переданного функции main . Я знал немало хороших инструментов из разных источников, однако ни один из них не делал 14 225 226 Глава 14 . Последовательное очищение именно того, что мне было нужно . Разумеется, я решил написать собственную реализацию — назовем ее Args Класс Args очень прост в использовании . Вы конструируете экземпляр класса Args с входными аргументами и форматной строкой, а затем обращаетесь к нему за значениями аргументов . Рассмотрим простой пример . листинг 14 .1 . Простое использование Args public static void main(String[] args) { try { Args arg = new Args("l,p#,d*", args); boolean logging = arg.getBoolean('l'); int port = arg.getInt('p'); String directory = arg.getString('d'); executeApplication(logging, port, directory); } catch (ArgsException e) { System.out.printf("Argument error: %s\n", e.errorMessage()); } } Вы и сами видите, что все действительно просто . Мы создаем экземпляр класса Args с двумя параметрами . Первый параметр задает форматную строку: " l,p#,d*. " Эта строка определяет три аргумента командной строки . Первый аргумент, –l , относится к логическому (булевскому) типу . Второй аргумент, -p , относится к целочисленному типу . Третий аргумент, -d , является строковым . Во втором параметре конструктора Args содержится массив аргументов командной строки, полученный main Если конструктор возвращает управление без выдачи исключения ArgsException , значит, разбор входной командной строки прошел успешно, и экземпляр Args готов к приему запросов . Методы getBoolean , getInteger , getString и т . д . исполь- зуются для получения значений аргументов по именам . При возникновении проблем (в форматной строке или в самих аргументах ко- мандной строки) инициируется исключение ArgsException . Для получения тексто- вого описания проблемы следует вызвать метод errorMessage объекта исключения . Реализация args Реализация класса Args приведена в листинге 14 .2 . Пожалуйста, очень внима- тельно прочитайте ее . Я основательно потрудился над стилем и структурой кода и надеюсь, что вы сочтете его достойным образцом для подражания . листинг 14 .2 . Args.java package com.objectmentor.utilities.args; import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; 226 Реализация Args 227 import java.util.*; public class Args { private Map private Set private ListIterator public Args(String schema, String[] args) throws ArgsException { marshalers = new HashMap argsFound = new HashSet parseSchema(schema); parseArgumentStrings(Arrays.asList(args)); } private void parseSchema(String schema) throws ArgsException { for (String element : schema.split(",")) if (element.length() > 0) parseSchemaElement(element.trim()); } private void parseSchemaElement(String element) throws ArgsException { char elementId = element.charAt(0); String elementTail = element.substring(1); validateSchemaElementId(elementId); if (elementTail.length() == 0) marshalers.put(elementId, new BooleanArgumentMarshaler()); else if (elementTail.equals("*")) marshalers.put(elementId, new StringArgumentMarshaler()); else if (elementTail.equals("#")) marshalers.put(elementId, new IntegerArgumentMarshaler()); else if (elementTail.equals("##")) marshalers.put(elementId, new DoubleArgumentMarshaler()); else if (elementTail.equals("[*]")) marshalers.put(elementId, new StringArrayArgumentMarshaler()); else throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail); } private void validateSchemaElementId(char elementId) throws ArgsException { if (!Character.isLetter(elementId)) throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null); } private void parseArgumentStrings(List { for (currentArgument = argsList.listIterator(); currentArgument.hasNext();) { String argString = currentArgument.next(); if (argString.startsWith("-")) { parseArgumentCharacters(argString.substring(1)); } else { currentArgument.previous(); продолжение 227 228 Глава 14 . Последовательное очищение листинг 14 .2 (продолжение) break; } } } private void parseArgumentCharacters(String argChars) throws ArgsException { for (int i = 0; i < argChars.length(); i++) parseArgumentCharacter(argChars.charAt(i)); } private void parseArgumentCharacter(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); if (m == null) { throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null); } else { argsFound.add(argChar); try { m.set(currentArgument); } catch (ArgsException e) { e.setErrorArgumentId(argChar); throw e; } } } public boolean has(char arg) { return argsFound.contains(arg); } public int nextArgument() { return currentArgument.nextIndex(); } public boolean getBoolean(char arg) { return BooleanArgumentMarshaler.getValue(marshalers.get(arg)); } public String getString(char arg) { return StringArgumentMarshaler.getValue(marshalers.get(arg)); } public int getInt(char arg) { return IntegerArgumentMarshaler.getValue(marshalers.get(arg)); } public double getDouble(char arg) { return DoubleArgumentMarshaler.getValue(marshalers.get(arg)); } public String[] getStringArray(char arg) { 228 Реализация Args 229 return StringArrayArgumentMarshaler.getValue(marshalers.get(arg)); } } Обратите внимание: код читается сверху вниз, и вам не приходится постоянно переходить туда-сюда или заглядывать вперед . Единственное место, где все же необходимо заглянуть вперед, — это определение ArgumentMarshaler , но и это было сделано намеренно . Внимательно прочитав этот код, вы поймете, что собой представляет интерфейс ArgumentMarshaler и что делают производные классы . Примеры таких классов приведены в листингах 14 .3–14 .6 . |