Главная страница
Навигация по странице:

  • Обеспечьте логическую изоляцию конфигураций многопоточного кода

  • Протестируйте программу на разных платформах

  • Рекомендация

  • Автоматизированная инструментовка

  • Последовательное очищение

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница26 из 49
    1   ...   22   23   24   25   26   27   28   29   ...   49
    Рекомендация: реализуйте свой многопоточный код так, чтобы он мог выпол-
    няться в различных конфигурациях.
    Обеспечьте логическую изоляцию конфигураций
    многопоточного кода
    Правильный баланс программных потоков обычно определяется методом проб и ошибок . Прежде всего найдите средства измерения производительности си- стемы в разных конфигурациях . Реализуйте систему так, чтобы количество программных потоков могло легко изменяться . Подумайте, нельзя ли разрешить его изменение во время работы системы . Рассмотрите возможность автомати- ческой настройки в зависимости от текущей производительности и загрузки системы .
    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 marshalers;
    private Set argsFound;
    private ListIterator currentArgument;
    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 argsList) throws ArgsException
    {
    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 .
    1   ...   22   23   24   25   26   27   28   29   ...   49


    написать администратору сайта