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

  • Вычисление производительности в однопоточной модели

  • Рис . А .1 .

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница41 из 49
    1   ...   37   38   39   40   41   42   43   44   ...   49
    373
    Через несколько недель отладки мы обнаружили, что причиной зависания была десинхронизация между счетчиком кольцевого буфера и его указателем . Буфер управлял выводом на терминал . Значение указателя говорило о том, что буфер был пуст, а счетчик утверждал, что буфер полон . Так как буфер был пуст, вы- водить было нечего; но так как одновременно он был полон, в экранный буфер нельзя было ничего добавить для вывода на экран .
    Итак, мы знали, почему зависали терминалы, но было неясно, из-за чего возникает десинхронизация кольцевого буфера . Поэтому мы реализовали обходное решение .
    Программный код мог прочитать состояние тумблеров на передней панели компью- тера (это было очень, очень, очень давно) . Мы написали небольшую функцию, которая обнаруживала переключение одного из тумблеров и искала кольцевой буфер, одно- временно пустой и заполненный . Обнаружив такой буфер, функция сбрасывала его в пустое состояние . Voila! Зависший терминал снова начинал выводить информацию .
    Теперь нам не приходилось перезагружать систему при зависании терминала .
    Когда нам звонили из управления и сообщали о зависании, мы шли в машинный зал и переключали тумблер .
    Конечно, иногда управление работало по выходным, а мы – нет . Поэтому в пла- нировщик была включена функция, которая раз в минуту проверяла состояние всех кольцевых буферов и сбрасывала те из них, которые были одновременно пустыми и заполненными . Зависший терминал начинал работать даже до того, как операторы успевали подойти к телефону .
    Прошло несколько недель кропотливого просеивания монолитного ассемблер- ного кода, прежде чем была обнаружена причина . Мы занялись вычислениями и определили, что частота зависаний статистически соответствует одному неза- щищенному использованию кольцевого буфера . Оставалось только найти это одно использование . К сожалению, все это было очень давно . В те времена у нас не было функций поиска, перекрестных ссылок или других средств автоматиза- ции . Нам просто приходилось просматривать листинги .
    Тогда, холодной зимой 1971 года в Чикаго, я узнал важный урок . Клиентская блокировка — полный отстой .
    Серверная блокировка
    Дублирование можно устранить внесением следующих изменений в
    Integer-
    Iterator
    :
    public class IntegerIteratorServerLocked {
    private Integer nextValue = 0;
    public synchronized Integer getNextOrNull() {
    if (nextValue < 100000)
    return nextValue++;
    else return null;
    }
    }
    373

    374
    Приложение А . Многопоточность II
    В клиентском коде также вносятся изменения:
    while (true) {
    Integer nextValue = iterator.getNextOrNull();
    if (next == null)
    break;
    // Действия с nextValue
    }
    В этом случае мы изменяем API своего класса, чтобы он обладал многопоточной поддержкой
    1
    . Вместо проверки hasNext()
    клиент должен выполнить проверку null
    В общем случае серверная блокировка предпочтительна по следующим при- чинам:
    
    Она сокращает дублирование кода – клиентская блокировка заставляет каж- дого клиента устанавливать соответствующую блокировку сервера . Если код блокировки размещается на сервере, клиенты могут использовать объект, не беспокоясь о написании дополнительного кода блокировки .
    
    Она обеспечивает более высокую производительность – в случае однопоточ- ного развертывания потоково-безопасный сервер можно заменить потоково- небезопасным, устраняя все дополнительные затраты .
    
    Она снижает вероятность ошибок – в случае клиентской блокировки доста- точно всего одному программисту забыть установить блокировку, и работа системы будет нарушена .
    
    Она определяет единую политику использования – политика сосредоточена в одном месте (на сервере), а не во множестве разных мест (то есть у каждого клиента) .
    Она сокращает область видимости общих переменных — клиент не знает ни о переменных, ни о том, как они блокируются . Все подробности скрыты на сторо- не сервера . Если что-то сломается, то количество мест, в которых следует искать причину, сокращается .
    Что делать, если серверный код вам неподконтролен?
    
    Используйте паттерн АДАПТЕР, чтобы изменить API и добавить блокировку:
    public class ThreadSafeIntegerIterator {
    private IntegerIterator iterator = new IntegerIterator();
    public synchronized Integer getNextOrNull() {
    if(iterator.hasNext())
    return iterator.next();
    return null;
    }
    }
    
    ИЛИ еще лучше – используйте потоково-безопасные коллекции с расширен- ными интерфейсами .
    1
    На самом деле интерфейс Iterator в принципе не обладает потоковой безопасностью . Он не проектировался с расчетом на многопоточное использование, так что этот факт не вы- зывает удивления .
    374

    Повышение производительности
    375
    Повышение производительности
    Допустим, вы хотите выйти в сеть и прочитать содержимое группы страниц по списку URL-адресов . По мере чтения страницы обрабатываются для накоп- ления некоторой статистики . После чтения всех страниц выводится сводный отчет .
    Следующий класс возвращает содержимое одной страницы по URL-адресу .
    public class PageReader {
    //...
    public String getPageFor(String url) {
    HttpMethod method = new GetMethod(url);
    try {
    httpClient.executeMethod(method);
    String response = method.getResponseBodyAsString();
    return response;
    } catch (Exception e) {
    handle(e);
    } finally {
    method.releaseConnection();
    }
    }
    }
    Следующий класс – итератор, предоставляющий содержимое страниц на осно- вании итератора URL-адресов:
    public class PageIterator {
    private PageReader reader;
    private URLIterator urls;
    public PageIterator(PageReader reader, URLIterator urls) {
    this.urls = urls;
    this.reader = reader;
    }
    public synchronized String getNextPageOrNull() {
    if (urls.hasNext())
    getPageFor(urls.next());
    else return null;
    }
    public String getPageFor(String url) {
    return reader.getPageFor(url);
    }
    }
    Экземпляр
    PageIterator может совместно использоваться разными потоками, каждый из которых использует собственный экземпляр
    PageReader для чтения и обработки страниц, полученных от итератора .
    375

    376
    Приложение А . Многопоточность II
    Обратите внимание: блок synchronized очень мал . Он содержит только критиче- скую секцию, расположенную глубоко внутри
    PageIterator
    . Старайтесь синхро- низировать как можно меньший объем кода .
    Вычисление производительности
    в однопоточной модели
    Выполним некоторые простые вычисления . Для наглядности возьмем следующие показатели:
    
    Время ввода/вывода для получения страницы (в среднем): 1 секунда .
    
    Время обработки страницы (в среднем): 0,5 секунды .
    
    Во время операций ввода/вывода процессор загружен на 0%, а во время об- работки – на 100% .
    При обработке N страниц в однопоточной модели общее время выполнения со- ставляет 1,5 секунды * N . На рис . А .1 изображен график обработки 13 страниц примерно за 19,5 секунды .
    Рис . А .1 . Обработка страниц в однопоточной модели
    Вычисление производительности
    в многопоточной модели
    Если страницы могут загружаться в произвольном порядке и обрабатываться независимо друг от друга, то для повышения производительности можно вос- пользоваться многопоточной моделью . Что произойдет, если обработка будет производиться тремя потоками? Сколько страниц удастся обработать за то же время?
    Как видно из рис . А .2, многопоточное решение позволяет совмещать процессорно- ориентированную обработку страниц с операциями чтения страниц, ориентиро- ванными на ввод/вывод . В идеальном случае это обеспечивало бы полную за- грузку процессора: каждое чтение страницы продолжительностью в одну секунду перекрывается с обработкой двух страниц . Таким образом, многопоточная модель обрабатывает две страницы в секунду, что втрое превышает производительность однопоточной модели .
    376

    Взаимная блокировка
    377
    Рис . А .2 . Обработка тремя параллельными потоками
    Взаимная блокировка
    Допустим, у нас имеется веб-приложение с двумя общими пулами ресурсов конечного размера:
    
    Пул подключений к базе данных для локальной обработки в памяти процесса .
    
    Пул подключений MQ к главному хранилищу .
    В работе приложения используются две операции, создание и обновление:
    
    Создание – получение подключений к главному хранилищу и базе данных .
    Взаимодействие с главным хранилищем и локальное сохранение данных в базе данных процесса .
    
    Обновление – получение подключений к базе данных, а затем к главному хранилищу . Чтение данных из базы данных процесса и их последующая пере- дача в главное хранилище .
    Что произойдет, если количество пользователей превышает размеры пулов? До- пустим, каждый пул содержит десять подключений .
    
    Десять пользователей пытаются использовать операцию создания . Они за- хватывают все десять подключений к базе данных . Выполнение каждого потока прерывается после захвата подключения к базе данных, но до захвата подключения к главному хранилищу .
    
    Десять пользователей пытаются использовать операцию обновления . Они захватывают все десять подключений к главному хранилищу . Выполнение каждого потока прерывается после захвата подключения к главному храни- лищу, но до захвата подключения к базе данных .
    377

    378
    Приложение А . Многопоточность II
    
    Десять потоков, выполняющих операцию создания, ожидают подключений к главному хранилищу, а десять потоков, выполняющих операцию обновле- ния, ожидают подключений к базе данных .
    
    Возникает взаимная блокировка . Продолжение работы системы невозможно .
    На первый взгляд такая ситуация выглядит маловероятной, но кому нужна система, которая гарантированно зависает каждую неделю? Кому захочется отлаживать систему с такими трудновоспроизводимыми симптомами? Когда такие проблемы возникают в эксплуатируемой системе, на их решение уходят целые недели .
    Типичное «решение» основано на включении отладочных команд для получения дополнительной информации о происходящем . Конечно, отладочные команды достаточно сильно изменяют код, взаимная блокировка возникает в другой си- туации, и на повторение ошибки могут потребоваться целые месяцы
    1
    Чтобы действительно решить проблему взаимной блокировки, необходимо понять, из-за чего она возникает . Для возникновения взаимной блокировки не- обходимы четыре условия:
    
    Взаимное исключение .
    
    Блокировка с ожиданием .
    
    Отсутствие вытеснения .
    
    Циклическое ожидание .
    Взаимное исключение
    Взаимное исключение возникает в том случае, когда несколько потоков должны использовать одни и те же ресурсы, и эти ресурсы:
    
    не могут использоваться несколькими потоками одновременно;
    
    существуют в ограниченном количестве .
    Типичный пример ресурсов такого рода – подключения к базам данных, откры- тые для записи файлы, блокировки записей, семафоры .
    Блокировка с ожиданием
    Когда один поток захватывает ресурс, он не освобождает его до тех пор, пока не захватит все остальные необходимые ресурсы и не завершит свою работу .
    Отсутствие вытеснения
    Один поток не может отнимать ресурсы у другого потока . Если поток захватил ресурс, то другой поток сможет получить захваченный ресурс только в одном случае: если первый поток его освободит .
    1
    Кто-то добавляет отладочный вывод, и проблема «исчезает» . Отладочный код «решил» проблему, поэтому он остается в системе .
    378

    Взаимная блокировка
    379
    циклическое ожидание
    Допустим, имеются два потока T1 и T2 и два ресурса R1 и R2 . Поток T1 захватил
    R1, поток T2 захватил R2 . Потоку T1 также необходим ресурс R2, а потоку T2 также необходим ресурс R1 . Ситуация выглядит так, как показано на рис . А .3 .
    Рис . А .3 . Циклическое ожидание
    Взаимная блокировка возможна только при соблюдении всех четырех условий .
    Стоит хотя бы одному из них нарушиться, и взаимная блокировка исчезнет .
    нарушение взаимного исключения
    Одна из стратегий предотвращения взаимной блокировки основана на предот- вращении состояния взаимного исключения .
    
    Использование ресурсов, поддерживающих многопоточный доступ (напри- мер,
    AtomicInteger
    ) .
    
    Увеличение количества ресурсов, чтобы оно достигло или превосходило ко- личество конкурирующих потоков .
    
    Проверка наличия всех свободных ресурсов перед попытками захвата .
    К сожалению, большинство ресурсов существует в ограниченном количестве и не поддерживает многопоточное использование . Кроме того, второй ресурс нередко определяется только по результатам обработки первого ресурса . Но не огорчайтесь – остаются еще три условия .
    нарушение блокировки с ожиданием
    Вы также можете нарушить взаимную блокировку, если откажетесь ждать . Про- веряйте каждый ресурс, прежде чем захватывать его; если какой-либо ресурс за- нят, освободите все захваченные ресурсы и начните все заново .
    При таком подходе возможны следующие потенциальные проблемы:
    
    Истощение – один поток стабильно не может захватить все необходимые ему ресурсы (уникальная комбинация, в которой все ресурсы одновременно оказываются свободными крайне редко) .
    379

    380
    Приложение А . Многопоточность II
    
    Обратимая блокировка – несколько потоков «входят в клинч»: все они захва- тывают один ресурс, затем освобождают один ресурс… снова и снова . Такая ситуация особенно вероятна при использовании тривиальных алгоритмов планирования процессорного времени (встроенные системы; простейшие, написанные вручную алгоритмы балансировки потоков) .
    Обе ситуации могут привести к снижению производительности . Первая ситуация оборачивается недостаточным, а вторая – завышенным и малоэффективным ис- пользованием процессора .
    Какой бы неэффективной ни казалась эта стратегия, она все же лучше, чем ниче- го . По крайней мере, вы всегда можете реализовать ее, если все остальные меры не дают результата .
    нарушение отсутствия вытеснения
    Другая стратегия предотвращения взаимных блокировок основана на том, чтобы потоки могли отнимать ресурсы у других потоков . Обычно вытеснение реали- зуется на основе простого механизма запросов . Когда поток обнаруживает, что ресурс занят, он обращается к владельцу с запросом на его освобождение . Если владелец также ожидает другого ресурса, он освобождает все удерживаемые ресурсы и начинает захватывать их заново .
    Данное решение сходно с предыдущим, но имеет дополнительное преимущество: поток может ожидать освобождения ресурса . Это снижает количество освобож- дений и повторных захватов . Но учтите, что правильно реализовать управление запросами весьма непросто .
    нарушение циклического ожидания
    Это самая распространенная стратегия предотвращения взаимных блокировок .
    В большинстве систем она не требует ничего, кроме системы простых правил, соблюдаемых всеми сторонами .
    Вспомните приведенный ранее пример с потоком 1, которому необходимы ресурс 1 и ресурс 2, и потоком 2, которому необходимы ресурс 2 и ресурс 1 . За- ставьте поток 1 и поток 2 выделять ресурсы в постоянном порядке, при котором циклическое ожидание станет невозможным .
    В более общем виде, если все потоки согласуют единый глобальный порядок и будут захватывать ресурсы только в указанном порядке, то взаимная блоки- ровка станет невозможной . Эта стратегия, как и все остальные, может создавать проблемы:
    
    Порядок захвата может не соответствовать порядку использования; таким образом, ресурс, захваченный в начале, может не использоваться до самого конца . В результате ресурсы остаются заблокированными дольше необходи- мого .
    380

    Тестирование многопоточного кода
    381
    
    Соблюдение фиксированного порядка захвата ресурсов возможно не всегда .
    Если идентификатор второго ресурса определяется на основании операций, выполняемых с первым ресурсом, то эта стратегия становится невозможной .
    Итак, существует много разных способов предотвращения взаимных блокировок .
    Одни приводят к истощению потоков, другие – к интенсивному использованию процессора и ухудшению времени отклика . Бесплатный сыр бывает только в мышеловке!
    Изоляция потокового кода вашего решения упрощает настройку и эксперименты; к тому же это действенный способ сбора информации, необходимой для опреде- ления оптимальных стратегий .
    тестирование многопоточного кода
    Как написать тест, демонстрирующий некорректность многопоточного кода?
    01: public class ClassWithThreadingProblem {
    02: int nextId;
    03:
    04: public int takeNextId() {
    05: return nextId++;
    06: }
    07:}
    Тест, доказывающий некорректность, может выглядеть так:
    
    Запомнить текущее значение nextId
    
    Создать два потока, каждый из которых вызывает takeNextId()
    по одному разу .
    
    Убедиться в том, что значение nextId на 2 больше исходного .
    
    Выполнять тест до тех пор, пока в ходе очередного теста nextId не увеличится только на 1 вместо 2 .
    Код такого теста представлен в листинге А .2 .
    листинг А .2 . ClassWithThreadingProblemTest.java
    01: package example;
    02:
    03: import static org.junit.Assert.fail;
    04:
    05: import org.junit.Test;
    06:
    07: public class ClassWithThreadingProblemTest {
    08: @Test
    09: public void twoThreadsShouldFailEventually() throws Exception {
    10: final ClassWithThreadingProblem classWithThreadingProblem
    = new ClassWithThreadingProblem();
    11:
    12: Runnable runnable = new Runnable() {
    продолжение
    381

    382
    Приложение А . Многопоточность II
    листинг А .2 (продолжение)
    13: public void run() {
    14: classWithThreadingProblem.takeNextId();
    15: }
    16: };
    17:
    18: for (int i = 0; i < 50000; ++i) {
    19: int startingId = classWithThreadingProblem.lastId;
    20: int expectedResult = 2 + startingId;
    21:
    22: Thread t1 = new Thread(runnable);
    23: Thread t2 = new Thread(runnable);
    24: t1.start();
    25: t2.start();
    26: t1.join();
    27: t2.join();
    28:
    29: int endingId = classWithThreadingProblem.lastId;
    30:
    31: if (endingId != expectedResult)
    32: return;
    33: }
    34:
    35: fail("Should have exposed a threading issue but it did not.");
    36: }
    37: }
    1   ...   37   38   39   40   41   42   43   44   ...   49


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