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

  • Листинг 12.5

  • Листинг 12.6

  • 12.1.4 Тестирование управления ресурсами

  • Листинг 12.7

  • 12.1.5 Использование обратных вызовов

  • Листинг 12.8

  • Листинг 12.9

  • Листинг 12.10

  • 12.2.1 Расширение класса PutTakeTest с добавлением учёта времени

  • При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница24 из 34
    1   ...   20   21   22   23   24   25   26   27   ...   34
    Листинг 12.4 Генератор случайных чисел среднего качества, подходящий для тестирования
    Класс
    PutTakeTest
    , представленный в листингах 12.5 и 12.6, запускает N
    потоков-производителей, которые генерируют элементы и помещают их в очередь, и N потоков-потребителей, которые извлекают элементы из очереди. Каждый поток обновляет контрольную сумму элементов по мере их поступления или изъятия, используя индивидуальную, для каждого потока, контрольную сумму, которая
    129
    Множество тестов на производительность, без ведома разработчиков или пользователей, фактически представляет собой тесты того, насколько велико узкое место в параллелизме, введённое использованием RNG.
    объединяется в конце выполнения теста, чтобы не добавлять больше синхронизации или конкуренции, чем требуется для тестирования буфера. public class PutTakeTest { private static final ExecutorService pool
    = Executors.newCachedThreadPool(); private final AtomicInteger putSum = new AtomicInteger(0); private final AtomicInteger takeSum = new AtomicInteger(0); private final CyclicBarrier barrier; private final BoundedBuffer bb; private final int nTrials, nPairs; public static void main(String[] args) { new PutTakeTest(10, 10, 100000).test(); // sample parameters pool.shutdown();
    }
    PutTakeTest(int capacity, int npairs, int ntrials) { this.bb = new BoundedBuffer(capacity); this.nTrials = ntrials; this.nPairs = npairs; this.barrier = new CyclicBarrier(npairs
    *
    2 + 1);
    } void test() { try { for (int i = 0; i < nPairs; i++) { pool.execute(new Producer()); pool.execute(new Consumer());
    } barrier.await(); // wait for all threads to be ready barrier.await(); // wait for all threads to finish assertEquals(putSum.get(), takeSum.get());
    } catch (Exception e) { throw new RuntimeException(e);
    }
    } class Producer implements Runnable { /
    *
    Listing 12.6
    *
    / } class Consumer implements Runnable { /
    *
    Listing 12.6
    *
    / }
    }
    Листинг 12.5 Тест на основе шаблона производитель-потребитель, для класса
    BoundedBuffer
    В зависимости от платформы создание и запуск потока может быть умеренно тяжелой операцией. Если ваш поток является кратковременным, и вы запускаете несколько потоков в цикле, в худшем случае потоки выполняются последовательно, а не параллельно. Тот факт, что даже не в самом худшем случае первый поток имеет преимущество над другими, означает, что вы можете получить меньше наложений, чем ожидалось: первый поток выполняется сам по себе в течение некоторого времени, а затем первые два потока, в течение некоторого
    времени, выполняются параллельно, и только со временем все потоки начинают работать параллельно. (То же самое происходит и в конце выполнения: потоки, которые получили фору, также завершаются раньше.)
    /
    *
    inner classes of PutTakeTest (Listing 12.5)
    *
    /
    class Producer implements Runnable { public void run() { try { int seed = (this.hashCode() ^ (int)System.nanoTime()); int sum = 0; barrier.await(); for (int i = nTrials; i > 0; --i) { bb.put(seed); sum += seed; seed = xorShift(seed);
    } putSum.getAndAdd(sum); barrier.await();
    } catch (Exception e) { throw new RuntimeException(e);
    }
    }
    } class Consumer implements Runnable { public void run() { try { barrier.await(); int sum = 0; for (int i = nTrials; i > 0; --i) { sum += bb.take();
    } takeSum.getAndAdd(sum); barrier.await();
    } catch (Exception e) { throw new RuntimeException(e);
    }
    }
    }
    Листинг 12.6 Классы производитель и потребитель, используемые в классе
    PutTakeTest
    Мы представили подход для смягчения этой проблемы в разделе
    5.5.1
    , используя один экземпляр
    CountDownLatch в качестве начального затвора, а другой - в качестве конечного. Другой способ получить тот же эффект заключается в том, чтобы использовать класс
    CyclicBarrier
    , инициализированный числом рабочих потоков плюс один, и имеющиеся рабочие потоки и тестовый драйвер, ожидающие у барьера в моменты начала и завершения выполнения. Это гарантирует, что все потоки запускаются и находятся в рабочем состоянии, до фактического начала выполнения работы. Класс
    PutTakeTest использует этот подход, чтобы координировать запуск и остановку рабочих потоков, потенциально создавая большее количество чередований параллелизма. Мы все еще не можем
    гарантировать, что планировщик не будет обрабатывать каждый поток последовательно, до самого завершения, но сделав выполнение достаточно длительным, мы уменьшили степень, с которой планирование вносит искажение в наши результаты.
    Последним трюком, использованным классом PutTakeTest, является использование детерминированного критерия завершения, так что никакой дополнительной координации между потоками, для выяснения, завершён ли тест, не требуется. Метод тестирования запускает ровно тоже количество производителей, как и потребителей, и каждый из них отправляет (
    put
    ) или принимает (
    take
    ) одинаковое количество элементов, поэтому общее количество добавленных и удаленных элементов одинаково.
    Тесты, подобные классу
    PutTakeTest
    , как правило, хороши при поиске нарушений безопасности. Например, распространенная ошибка при реализации буферов, управляемых семафорами, заключаются в том, что забывают о том, что код, фактически выполняющий вставку и извлечение, требует взаимного исключения (с помощью блока synchronized или с использованием класса
    ReentrantLock
    ). Пример запуска класса
    PutTakeTest с версией класса
    BoundedBuffer
    , в которой опущена синхронизация методов doInsert и doExtract, довольно быстро терпит неудачу. Запуск класса
    PutTakeTest с несколькими десятками потоков, выполняющими итерацию несколько миллионов раз на буферах различной мощности, в различных системах, повышает нашу уверенность в отсутствии повреждения данных в методах put и take
    Тесты должны выполняться на многопроцессорных системах для потенциального увеличения разнообразия чередования. Однако наличие более чем нескольких процессоров не обязательно делает тесты более эффективными. Чтобы максимально увеличить вероятность обнаружения гонки данных, зависящей от момента времени, должно быть больше активных потоков, чем кол-во доступных ЦП, чтобы в любой момент времени некоторые потоки выполнялись, а некоторые отключались, что снижает предсказуемость взаимодействий между потоками.
    Для тестов, выполняемых до тех пор, пока не будет выполнено фиксированное число операций, возможна ситуация, при которой тестовый случай никогда не завершится, если тестируемый код поймает исключение из-за возникновения ошибки. Самый распространенный способ это исправить заключается в том, чтобы тестовый фреймворк прерывал процесс тестирования, который не завершается в течение определенного промежутка времени; сколько времени ожидать завершения, следует определять опытным путем, а сбои должны быть проанализированы, чтобы убедиться, что проблема заключается не в том, что вы ждёте не достаточно долго. (Эта проблема не является уникальной для процесса тестирования параллельных классов; последовательные тесты также должны различать длительные и бесконечные циклы.)
    12.1.4 Тестирование управления ресурсами
    Тесты до сих пор были связаны с соблюдением классом его спецификации – контроль того, что он делает то, что он должен делать. Второстепенным аспектом тестирования является проверка того, что он не делает тех вещей, которые он не
    должен делать, например, такие как утечка ресурсов. Любой объект, который содержит другие объекты или управляет ими, не должен продолжать удерживать ссылки на эти объекты дольше, чем необходимо. Такие утечки памяти не позволяют сборщикам мусора освобождать память (или потоки, дескрипторы файлов, сокеты, подключения к базе данных или другие ограниченные ресурсы) и могут привести к исчерпанию ресурсов и сбою приложения.
    Проблемы управления ресурсами особенно важны для классов, подобных классу
    BoundedBuffer
    – в целом причина ограничения буфера заключается в предотвращении сбоя приложения из-за исчерпания ресурсов, когда производители слишком опережают потребителей.
    Ограничение заставляет чрезмерно продуктивных производителей блокироваться, а не продолжать создавать работу, которая будет потреблять все больше и больше памяти или других ресурсов.
    Нежелательное удержание памяти можно легко протестировать с помощью инструментов проверки кучи, которые измеряют использование памяти приложением; это можно сделать с помощью различных коммерческих инструментов профилирования кучи, или с помощью инструментов с открытым исходным кодом. Метод testLeak в листинге 12.7 содержит маркеры для создания снимка кучи инструментом инспектирования кучи, который принудительно запускает сборщик мусора
    130
    , а затем записывает информацию о размере кучи и использовании памяти. class Big { double[] data = new double[100000]; } void testLeak() throws InterruptedException {
    BoundedBuffer bb = new BoundedBuffer(CAPACITY); int heapSize1 = /
    *
    snapshot heap
    *
    /; for (int i = 0; i < CAPACITY; i++) bb.put(new Big()); for (int i = 0; i < CAPACITY; i++) bb.take(); int heapSize2 = /
    *
    snapshot heap
    *
    /; assertTrue(Math.abs(heapSize1-heapSize2) < THRESHOLD);
    }
    Листинг 12.7 Тестирование на предмет утечек ресурсов
    Метод testLeak вставляет несколько больших объектов в ограниченный буфер, а затем удаляет их; использование памяти в моментальном снимке кучи #2 должно быть примерно таким же, как в моментальном снимке кучи #1. С другой стороны, если метод doExtract забыл обнулить ссылку на возвращаемый элемент
    (
    items[i]=null
    ), использование памяти в двух моментальных снимках, определенно, не будет одинаковым. (Это один из немногих случаев, когда необходимо явное обнуление; большую часть времени оно либо не полезно, либо фактически вредно [EJ пункт 5].)
    130
    Технически, невозможно заставить сборщик мусора начать уборку; метод
    System.gc только
    предлагает JVM выполнить уборку, так как это может быть хорошим моментом времени для выполнения сборки мусора. Среда HotSpot может быть проинструктирована игнорировать вызов метода
    System.gc
    , с помощью параметров запуска командной
    -XX:+DisableExplicitGC

    12.1.5 Использование обратных вызовов
    Обратные вызовы к клиентскому коду могут быть полезны при построении тестовых случаев; обратные вызовы часто выполняются в известных точках жизненного цикла объекта, которые предоставляют хорошие возможностями для утверждения инвариантов. Например, класс
    ThreadPoolExecutor выполняет вызовы задач - экземпляров
    Runnable
    , а также экземпляра
    ThreadFactory
    Тестирование пула потоков включает в себя тестирование ряда элементов политики выполнения: проверка того, что дополнительные потоки создаются только тогда, когда это ожидается, но не тогда, когда они не ожидаются; что простаивающие потоки пожинаются, когда это необходимо, и т.д. Создание комплексного набора тестов, охватывающего все возможности, является серьезной задачей, но многие аспекты могут быть достаточно просто протестированы индивидуально.
    Мы можем инструментировать создание потока, путем использования собственной фабрики потока. Класс
    TestingThreadFactory представленный в листинге 12.8, поддерживает подсчёт количества созданных потоков; затем тестовые случаи могут проверить количество потоков, созданных во время тестового запуска. Класс
    TestingThreadFactory может быть расширен, чтобы вернуть собственную реализацию класса
    Thread
    , который также записывает, когда поток завершается, так что тестовые случаи могут проверить, что потоки пожинаются в соответствии с политикой выполнения. class TestingThreadFactory implements ThreadFactory { public final AtomicInteger numCreated = new AtomicInteger(); private final ThreadFactory factory
    = Executors.defaultThreadFactory(); public Thread newThread(Runnable r) { numCreated.incrementAndGet(); return factory.newThread(r);
    }
    }
    Листинг 12.8 Фабрика потоков для тестирования класса
    ThreadPoolExecutor
    Если корневой размер пула меньше максимального размера, размер пула потоков должен увеличиваться по мере увеличения спроса на выполнение. При отправке долговременных задач в пул потоков, количество выполняемых задач остается постоянным достаточно долго, так что можно сделать несколько утверждений, например, что пул расширяется должным образом, как показано в листинге 12.9. public void testPoolExpansion() throws InterruptedException { int
    MAX_SIZE = 10;
    ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE); for (int i = 0; i < 10
    *
    MAX_SIZE; i++) exec.execute(new Runnable() { public void run() { try {

    Thread.sleep(Long.MAX_VALUE);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    }); for (int i = 0; i < 20 && threadFactory.numCreated.get() < MAX_SIZE; i++)
    Thread.sleep(100); assertEquals(threadFactory.numCreated.get(), MAX_SIZE); exec.shutdownNow();
    }
    Листинг 12.9 Тестовый метод для проверки расширения пула потоков
    12.1.6 Увеличение степени чередования
    Поскольку многие из потенциальных сбоев в параллельном коде являются маловероятными событиями, тестирование на наличие ошибок параллелизма - это игра чисел, но есть некоторые вещи, которые вы можете сделать, чтобы улучшить свои шансы. Мы уже упоминали о том, что работа на многопроцессорных системах с меньшим количеством процессоров, чем количество активных потоки, может генерировать больше чередований, чем в случае однопроцессорной системы или одного потока со многими процессорами. Аналогичным образом, тестирование на множестве систем с разным количеством процессоров, операционных систем и процессорных архитектур может выявить проблемы, которые могут возникать не во всех системах.
    Полезный трюк для увеличения числа чередований и, следовательно, более эффективного изучения пространства состояний ваших программ, заключается в использовании метода
    Thread.yield
    , для поощрения большего количества переключений контекста во время операций, которые обращаются к совместно используемому состоянию. (Эффективность этого метода зависит от платформы, так как JVM свободна в своём решении, обрабатывать вызов
    Thread.yield как no- op [JLS 17.9]; использование короткого, но ненулевого вызова sleep было бы медленнее, но более надежным.) Метод, представленный в листинге 12.10, осуществляет передачу кредитов с одного счета на другой; между двумя операциями обновления, инварианты подобные “сумма всех счетов равна нулю” не проводятся
    131
    . Иногда, уступая в процессе выполнения операции, вы можете активировать чувствительные ко времени ошибки в коде, который не использует адекватную синхронизацию для доступа к состоянию. Неудобство добавления этих вызовов для тестирования и удаления их в продуктиве может быть уменьшено, путем добавления их с помощью инструментов аспектно-ориентированного программирования (AOP). public synchronized void transferCredits(Account from,
    Account to, int amount) {
    131
    Имеется в виду банковская операция – проводка.
    from.setBalance(from.getBalance() - amount); if (random.nextInt(1000) > THRESHOLD)
    Thread.yield(); to.setBalance(to.getBalance() + amount);
    }
    Листинг 12.10 Использование метода
    Thread.yield для увеличения степени чередования
    12.2 Тестирование производительности
    Тесты производительности часто представляют собой расширенные версии тестов функциональности. На самом деле, почти всегда стоит включать в тесты производительности базовое тестирование функциональности, чтобы убедиться, что вы не тестируете производительность сломанного кода.
    Хотя между тестами производительности и функциональными тестами определенно есть перекрытие, у них разное предназначение. Тесты производительности предназначены для измерения сквозных показателей производительности для репрезентативных вариантов использования. Выбор разумного набора сценариев использования не всегда прост; в идеале тесты должны отражать фактическое использование тестируемых объектов в приложении.
    В некоторых случаях соответствующий сценарий тестирования очевиден.
    Ограниченные буферы почти всегда используются в дизайне производитель- потребитель, поэтому имеет смысл измерять пропускную способность производителей, передающих данные потребителям. Мы можем легко расширить класс
    PutTakeTest
    , чтобы превратить его в тест производительности для предложенного сценария.
    Общая вторичная цель тестирования производительности заключается в эмпирическом выборе размеров, для различных граничных условий - количества потоков, ёмкости буфера и т. д. Хотя эти значения могут оказаться достаточно чувствительными к характеристикам платформы (таким как тип процессора или даже уровень пошагового выполнения процессора, количеству ЦП или объему памяти) и требовать динамической конфигурации, разумные варианты для этих значений достаточно часто хорошо работают в широком диапазоне систем.
    12.2.1 Расширение класса PutTakeTest с добавлением учёта
    времени
    Основное расширение, которое мы должны сделать в классе
    PutTakeTest
    , это добавить возможность измерения времени, затрачиваемого на выполнение. Вместо того, чтобы пытаться измерить время, затрачиваемое на выполнение одной операции, мы получаем более точное измерение, путем учёта времени, в целом затрачиваемого на выполнение и деления полученного значения на количество операций, чтобы рассчитать время, затрачиваемое на выполнение одной операции.
    Ранее мы уже использовали класс
    CyclicBarrier для запуска и остановки рабочих потоков, поэтому мы можем расширить его с помощью барьерного действия, измеряющего время начала и окончания выполнения, как показано в листинге
    12.11. public class BarrierTimer implements Runnable { private boolean started;
    private long startTime, endTime; public synchronized void run() { long t = System.nanoTime(); if (!started) { started = true; startTime = t;
    } else endTime = t;
    } public synchronized void clear() { started = false;
    } public synchronized long getTime() { return endTime - startTime;
    }
    }
    1   ...   20   21   22   23   24   25   26   27   ...   34


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