Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
212 Глава 13 . Многопоточность исходят обращения к общему объекту, ключевым словом synchronized . Количество критических секций в коде должно быть сведено к минимуму . Чем больше в про- грамме мест, в которых обновляются общие данные, тем с большей вероятностью: вы забудете защитить одно или несколько из этих мест, что приведет к на- рушению работы всего кода, изменяющего общие данные . попытки уследить за тем, чтобы все было надежно защищено, приведут к дуб- лированию усилий (нарушение принципа DRY [PRAG]) . Вам будет труднее определить источник многопоточных сбоев, который и так достаточно сложно найти . Рекомендация: серьезно относитесь к инкапсуляции данных; жестко ограничьте доступ ко всем общим данным. Следствие: используйте копии данных Как избежать нежелательных последствий одновременного доступа к данным? Например, просто не использовать его . Существуют разные стратегии: напри- мер, в одних ситуациях можно скопировать общий объект и ограничить доступ к копии (доступ только для чтения) . В других ситуациях объекты копируются, результаты работы нескольких программных потоков накапливаются в копиях, а затем объединяются в одном потоке . Если существует простой способ избежать одновременного доступа к объектам, то вероятность возникновения проблем в полученном коде значительно снижает- ся . Вас беспокоят затраты на создание лишних объектов? Поэкспериментируйте и выясните, действительно ли она так высока . Как правило, если копирование объектов позволяет избежать синхронизации в коде, экономия на защитных блокировках быстро окупит дополнительные затраты на создание объектов и уборку мусора . Следствие: потоки должны быть как можно более независимы Постарайтесь писать многопоточный код так, чтобы каждый поток существовал в собственном замкнутом пространстве и не использовал данные совместно с дру- гими процессами . Каждый поток обрабатывает один клиентский запрос, все его данные берутся из отдельного источника и хранятся в локальных переменных . В этом случае каждый поток работает так, словно других потоков не существует, а следовательно, нет и требований к синхронизации . Например, классы, производные от HttpServlet , получают всю информацию в параметрах, передаваемых методам doGet и doPost . В результате каждый сервлет действует так, словно в его распоряжении находится отдельный компьютер . Если код сервлета ограничивается одними локальными переменными, он ни при каких условиях не вызовет проблем синхронизации . Конечно, большинство приложе- 212 Знайте свою библиотеку 213 ний, использующих сервлеты, рано или поздно сталкиваются с использованием общих ресурсов — например, подключений к базам данных . Рекомендация: постарайтесь разбить данные не независимые подмножества, с ко- торыми могут работать независимые потоки (возможно, на разных процессорах) . Знайте свою библиотеку В Java 5 возможности многопоточной разработки были значительно расширены по сравнению с предыдущими версиями . При написании многопоточного кода в Java 5 следует руководствоваться следующими правилами: Используйте потоково-безопасные коллекции . Используйте механизм Executor Framework для выполнения несвязанных задач . По возможности используйте неблокирующие решения . Некоторые библиотечные классы не являются потоково-безопасными . Потоково-безопасные коллекции Когда язык Java был еще молод, Даг Ли написал основополагающую книгу «Concurrent Programming in Java» [Lea99] . В ходе работы над книгой он разра- ботал несколько потоково-безопасных коллекций, которые позднее были вклю- чены в JDK в пакете java.util.concurrent . Коллекции этого пакета безопасны в условиях многопоточного выполнения, к тому же они достаточно эффективно работают . Более того, реализация ConcurrentHashMap почти всегда работает лучше HashMap . К тому же она поддерживает возможность выполнения параллельных операций чтения и записи и содержит методы для выполнения стандартных со- ставных операций, которые в общем случае не являются потоково-безопасными . Если ваша программа будет работать в среде Java 5, используйте ConcurrentHashMap в разработке . Также в Java 5 были добавлены другие классы для поддержки расширенной многопоточности . Несколько примеров . ReentrantLock Блокировка, которая может устанавливаться и освобождаться в разных методах Semaphore Реализация классического семафора (блокировка со счетчиком) CountDownLatch Блокировка, которая ожидает заданного количества событий до освобож- дения всех ожидающих потоков. Позволяет организовать более или менее одновременный запуск нескольких потоков Рекомендация: изучайте доступные классы. Если вы работаете на Java, уделите особое внимание пакетам java.util.concurrent , java.util.concurrent.atomic и java. util.concurrent.locks 213 214 Глава 13 . Многопоточность Знайте модели выполнения В многопоточных приложениях возможно несколько моделей логического раз- биения поведения программы . Но чтобы понять их, необходимо сначала позна- комиться с некоторыми базовыми определениями . Связанные ресурсы Ресурсы с фиксированным размером или количеством, существующие в многопоточной среде, например подключения к базе данных или бу- феры чтения/записи Взаимное исключение В любой момент времени с общими данными или с общим ресурсом может работать только один поток Зависание Работа одного или нескольких потоков приостанавливается на слишком долгое время (или навсегда). Например, если высокоприоритетным по- токам всегда предоставляется возможность отработать первыми, то низ- коприоритетные потоки зависнут (при условии, что в системе постоянно появляются новые высокоприоритетные потоки) Взаимная блокировка (deadlock) Два и более потока бесконечно ожидают завершения друг друга. Каждый поток захватил ресурс, необходимый для продолжения работы другого потока, и ни один поток не может завершиться без получения захвачен- ного другим потоком ресурса Обратимая блокировка 1 (livelock) Потоки не могут «разойтись» — каждый из потоков пытается выполнять свою работу, но обнаруживает, что другой поток стоит у него на пути. По- токи постоянно пытаются продолжить выполнение, но им это не удается в течение слишком долгого времени (или вообще не удается) Вооружившись этими определениями, можно переходить к обсуждению различ- ных моделей выполнения, встречающихся в многопоточном программировании . Модель «производители-потребители» 1 Один или несколько потоков-производителей создают задания и помещают их в буфер или очередь . Один или несколько потоков-потребителей извлекают задания из очереди и выполняют их . Очередь между производителями и по- требителями является связанным ресурсом . Это означает, что производители перед записью должны дожидаться появления свободного места в очереди, а по- требители должны дожидаться появления заданий в очереди для обработки . Координация производителей и потребителей основана на передаче сигналов . Производитель записывает задание и сигнализирует о том, что очередь не пуста . Потребитель читает задание и сигнализирует о том, что очередь не заполнена . Обе стороны должны быть готовы ожидать оповещения о возможности про- должения работы . 1 Также встречается термин «активная блокировка» . — Примеч. перев . 2 http://en .wikipedia .org/wiki/Producer-consumer 214 Знайте модели выполнения 215 Модель «читатели-писатели» 1 Если в системе имеется общий ресурс, который в основном служит источни- ком информации для потоков-«читателей», но время от времени обновляется потоками-«писателями», на первый план выходит проблема оперативности обновления . Если обновление будет происходить недостаточно часто, это может привести к зависанию и накоплению устаревших данных . С другой стороны, слишком частые обновления влияют на производительность . Координация работы читателей так, чтобы они не пытались читать данные, обновляемые пи- сателями, и наоборот, — весьма непростая задача . Писатели обычно блокируют работу многих читателей в течение долгого периода времени, а это отражается на производительности . Проектировщик должен найти баланс между потребностями читателей и писате- лей, чтобы обеспечить правильный режим работы, нормальную производитель- ность системы и избежать зависания . В одной из простых стратегий писатели дожидаются, пока в системе не будет ни одного читателя, и только после этого выполняют обновление . Однако при постоянном потоке читателей такая страте- гия приведет к зависанию писателей . С другой стороны, при большом количестве высокоприоритетных писателей пострадает производительность . Поиск баланса и предотвращение ошибок многопоточного обновления — основные проблемы этой модели выполнения . Модель «обедающих философов» 2 Представьте нескольких философов, сидящих за круглым столом . Слева у каж- дого философа лежит вилка, а в центре стола стоит большая тарелка спагетти . Философы проводят время в размышлениях, пока не проголодаются . Проголо- давшись, философ берет вилки, лежащие по обе стороны, и приступает к еде . Для еды необходимы две вилки . Если сосед справа или слева уже использует одну из необходимых вилок, философу приходится ждать, пока сосед закончит есть и положит вилки на стол . Когда философ поест, он кладет свои вилки на стол и снова погружается в размышления . Заменив философов программными потоками, а вилки — ресурсами, мы получа- ем задачу, типичную для многих корпоративных систем, в которых приложения конкурируют за ресурсы из ограниченного набора . Если небрежно отнестись к проектированию такой системы, то конкуренция между потоками может при- вести к возникновению взаимных блокировок, обратимых блокировок, падению производительности и эффективности работы . Большинство проблем многопоточности, встречающихся на практике, обычно представляют собой те или иные разновидности этих трех моделей . Изучайте 1 http://en .wikipedia .org/wiki/Readers-writers_problem 2 http://en .wikipedia .org/wiki/Dining_philosophers_problem 215 216 Глава 13 . Многопоточность алгоритмы, самостоятельно создавайте их реализации, чтобы столкнувшись с этими проблемами, вы были готовы к их решению . Рекомендация: изучайте базовые алгоритмы, разбирайтесь в решениях. Остерегайтесь зависимостей между синхронизированными методами Зависимости между синхронизированными методами приводят к появлению коварных ошибок в многопоточном коде . В языке Java существует ключевое слово synchronized для защиты отдельных методов . Но если общий класс со- держит более одного синхронизированного метода, возможно, ваша система спроектирована неверно 1 Рекомендация: избегайте использования нескольких методов одного совместно используемого объекта. Впрочем, иногда без использования разных методов одного общего объекта обой- тись все же не удается . Для обеспечения правильности работы кода в подобных ситуациях существуют три стандартных решения: Блокировка на стороне клиента — клиент устанавливает блокировку для сервера перед вызовом первого метода и следит за тем, чтобы блокировка распространялась на код, вызывающий последний метод . Блокировка на стороне сервера — на стороне сервера создается метод, который блокирует сервер, вызывает все методы, после чего снимает блокировку . Этот новый метод вызывается клиентом . Адаптирующий сервер — в системе создается посредник, который реализует блокировку . Ситуация может рассматриваться как пример блокировки на стороне сервера, в которой исходный сервер не может быть изменен . Синхронизированные секции должны иметь минимальный размер Ключевое слово synchronized устанавливает блокировку . Все секции кода, за- щищенные одной блокировкой, в любой момент времени гарантированно вы- полняются только в одном программном потоке . Блокировки обходятся дорого, так как они создают задержки и увеличивают затраты ресурсов . Следовательно, код не должен перегружаться лишними конструкциями synchronized . С другой 1 См . раздел «Зависимости между методами могут нарушить работу многопоточного кода», с . 370 . 216 О трудности корректного завершения 217 стороны, все критические секции 1 должны быть защищены . Следовательно, код должен содержать как можно меньше критических секций . Для достижения этой цели некоторые наивные программисты делают свои кри- тические секции очень большими . Однако синхронизация за пределами мини- мальных критических секций увеличивает конкуренцию между потоками и сни- жает производительность 2 Рекомендация: синхронизированные секции в ваших программах должны иметь минимальные размеры. О трудности корректного завершения Написание системы, которая должна работать бесконечно, заметно отличается от написания системы, которая работает в течение некоторого времени, а затем корректно завершается . Реализовать корректное завершение порой бывает весьма непросто . Одна из типичных проблем — взаимная блокировка 3 программных потоков, бесконечно долго ожидающих сигнала на продолжение работы . Представьте систему с родительским потоком, который порождает несколько дочерних потоков, а затем дожидается их завершения, чтобы освободить свои ресурсы и завершиться . Что произойдет, если один из дочерних потоков попадет во взаимную блокировку? Родитель будет ожидать вечно, и система не сможет корректно завершиться . Или возьмем аналогичную систему, получившую сигнал о завершении . Родитель приказывает всем своим потомкам прервать свои операции и завершить работу . Но что если два потомка составляют пару «производитель/потребитель»? Допустим, производитель получает сигнал от родителя, и прерывает свою работу . Потреби- тель, в этот момент ожидавший сообщения от производителя, блокируется в состо- янии, в котором он не может получить сигнал завершения . В результате он пере- ходит в бесконечное ожидание — а значит, родитель тоже не сможет завершиться . Подобные ситуации вовсе не являются нетипичными . Если вы пишете много- поточный код, который должен корректно завершаться, не жалейте времени на обеспечение нормального завершения работы . Рекомендация: начинайте думать о корректном завершении на ранней ста- дии разработки. На это может уйти больше времени, чем вы предполагаете. Проанализируйте существующие алгоритмы, потому что эта задача сложнее, чем кажется . 1 «Критической секцией» называется любой фрагмент кода, который должен быть защищен от одновременного использования несколькими программными потоками . 2 См . раздел «Увеличение производительности», с . 375 . 3 См . раздел «Взаимная блокировка», с . 377 . 217 218 Глава 13 . Многопоточность тестирование многопоточного кода Тестирование не гарантирует правильности работы кода . Тем не менее каче- ственное тестирование сводит риск к минимуму . Для однопоточных решений эти утверждения безусловно верны . Но как только в системе появляются два и более потока, использующие общий код и работающих с общими данными, ситуация значительно усложняется . Рекомендация: пишите тесты, направленные на выявление существующих проблем. Часто выполняйте их для разных вариантов программных/системных конфигураций и уровней нагрузки. Если при выполнении теста происходит ошибка, обязательно найдите причину. Не игнорируйте ошибку только потому, что при следующем запуске тест был выполнен успешно. Несколько более конкретных рекомендаций: Рассматривайте непериодические сбои как признаки возможных проблем многопоточности . Начните с отладки основного кода, не связанного с многопоточностью . Реализуйте логическую изоляцию конфигураций многопоточного кода . Обеспечьте возможность настройки многопоточного кода . Протестируйте программу с количеством потоков, превышающим количество процессоров . Протестируйте программу на разных платформах . Применяйте инструментовку кода для повышения вероятности сбоев . Рассматривайте непериодические сбои как признаки возможных проблем многопоточности В многопоточном коде сбои происходят даже там, где их вроде бы и быть не может . Многие разработчики (в том числе и автор) не обладают интуитивным представлением о том, как многопоточный код взаимодействует с другим кодом . Ошибки в многопоточном коде могут проявляться один раз за тысячу или даже миллион запусков . Воспроизвести такие ошибки в системе бывает очень трудно, поэтому разработчики часто склонны объяснять их «фазами Луны», случайными сбоями оборудования или другими несистематическими причинами . Однако игнорируя существование этих «разовых» сбоев, вы строите свой код на потен- циально ненадежном фундаменте . Рекомендация: не игнорируйте системные ошибки, считая их случайными, разо- выми сбоями. 218 Тестирование многопоточного кода 219 начните с отладки основного кода, не связанного с многопоточностью На первый взгляд совет выглядит тривиально, но еще раз подчеркнуть его зна- чимость не лишне . Убедитесь в том, что сам код работает вне многопоточного контекста . В общем случае это означает создание POJO-объектов, вызываемых из потоков . POJO-объекты не обладают поддержкой многопоточности, а следо- вательно, могут тестироваться вне многопоточной среды . Чем больше системного кода можно разместить в таких POJO-объектах, тем лучше . Рекомендация: не пытайтесь одновременно отлавливать ошибки в обычном и многопоточном коде . Убедитесь в том, что ваш код работает за пределами многопоточной среды выполнения . Реализуйте переключение конфигураций многопоточного кода Напишите вспомогательный код поддержки многопоточности, который может работать в разных конфигурациях . Один поток; несколько потоков; количество потоков изменяется по ходу вы- полнения . Многопоточный код взаимодействует с реальным кодом или тестовыми за- менителями . Код выполняется с тестовыми заменителями, которые работают быстро; мед- ленно; с переменной скоростью . Настройте тесты таким образом, чтобы они могли выполняться заданное количество раз . |