Текст книги "Программирование мобильных устройств на платформе .NET Compact Framework"
Автор книги: Иво Салмре
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 26 (всего у книги 69 страниц)
ГЛАВА 9
Производительность и многопоточное выполнение
Время пожирает все.
Овидий (43 до н.э.–17 н.э.), римский поэт(Encarta 2004, Quotations)
Введение: когда и как следует использовать многопоточное выполнение
Поскольку устройства используются на протяжении частых, но коротких рабочих сеансов, пользователи требуют, чтобы мобильное программное обеспечение реагировало на их запросы с минимальными задержками. Проще говоря, пользователи не желают понапрасну тратить время на ожидание ответной реакции устройства. Применение фоновых потоков позволяет добиться того, чтобы функции, обеспечивающие интерактивное взаимодействия пользователя с приложением, всегда выполнялись на переднем плане. Создание и использование фоновых потоков значительно расширяет возможности разработки сложных мобильных приложений. Однако это означает для вас как хорошие, так и плохие новости.
Как водится, сначала – плохие новости. За некоторыми исключениями, от введения дополнительных потоков ваш код работать быстрее не будет. Более того, в абсолютном смысле дополнительные потоки почти всегда лишь замедляют выполнение кода. Это происходит потому, что вы предоставляете операционной системе еще один поток, который ей приходится обслуживать, временами переключаясь на него. Переключение системы между различными потоками отрицательно сказывается на производительности.
Далее – опять плохие новости. Введение дополнительных потоков значительно увеличивает сложность кода вашего приложения и повышает вероятность появления в нем ошибок, связанных с синхронизацией выполнения потоков. Обнаруживать ошибки синхронизации чрезвычайно трудно. Если вы только начинаете экспериментировать с потоками, то вам легко будет впасть в соблазн увидеть в них ответы на все вопросы, с какими бы фактическими нуждами приложения это ни было связано. Стоит разработчику научиться работать с потоками, как они его "очаровывают", и он ищет малейший повод для их создания и применения. Тенденция к злоупотреблению фоновыми потоками становится еще более заметной при групповой разработке проектов; перед каждым членом группы стоит своя задача, для которой ему хотелось бы выделить независимый поток, чтобы нужная работа могла выполняться независимо от выполнения какого-либо другого кода. Поначалу такая идея действительно может казаться вполне разумной, но это до тех пор, пока все части кода не начнут работать вместе и приложению не придется выполнять одновременно, скажем, семь потоков, делая чрезвычайно красивые, но совершенно ненужные вещи. Дополнительные потоки, в которых нет крайней необходимости, лишь замедляют работу приложения и существенно усложняют его. Чтобы поддержать производительность приложения на высоком уровне, разработчики группового проекта должны продумать способ, позволяющий эффективно распределять задачи между конечным числом потоков или устанавливать очередность их выполнения.
А теперь – хорошие новости. Фоновые потоки могут быть весьма полезными. Благодаря фоновым потокам вы можете организовать в одном приложении несколько потоков выполнения команд приложения. Микропроцессор предоставляет каждому потоку квант времени для выполнения, и еще некоторое время тратится на переключение между потоками. Несмотря на то что общее количество инструкций программы, выполненных несколькими параллельными потоками, всегда будет меньше, чем в случае их выполнения единственным потоком, которому предоставлены все кванты времени, два потока могут решать параллельно разные задачи. Иногда такая возможность оказывается весьма ценной.
Дополнительные один-два потока могут улучшить способность вашего приложения к отклику за счет того, что задачи можно выполнять в фоновом режиме, а высокоприоритетный поток переднего плана оставить для обеспечения обратной связи приложения с пользователем. В качестве аналогии рассмотрим пример гостиницы. Чтобы гостиница функционировала эффективно, за стойкой портье всегда должен находиться человек. Посетители могут подойти к стойке со своими просьбами или вопросами в любое время дня и ночи, и один сотрудник гостиницы должен быть всегда свободен, чтобы незамедлительно обслужить посетителя. Портье можно сравнить с интерфейсом пользовательского приложения. Всякий раз, когда возникает задача, для выполнения которой требуется определенное время, портье перепоручает эту работу кому-то другому, чтобы сохранить для себя возможность обслуживания других посетителей. Другие сотрудники гостиницы играют роль фоновых потоков выполнения. Никакая работа не выполняется бесплатно. Чтобы иметь возможность нанять дополнительных людей и постоянно держать портье у стойки, гостиница должна платить им зарплату. В случае мобильного устройства ваши дополнительные накладные расходы покрываются за счет бюджета общей производительности приложения. У вашего приложения имеется ограниченный ресурс вычислительной мощности, и он должен разумно распределяться между высокоприоритетным потоком переднего плана, фоновыми задачами и обслуживанием переключения между потоками.
Если в приложении имеются длительно выполняющиеся алгоритмы, завершение которых необходимо для получения аналитического ответа, то фоновый поток является великолепным кандидатом для выполнения этой работы. Так, в процессе алгоритмического выбора следующего хода в шахматной игре значительное время тратится на сравнение различных вариантов ходов; фоновый поток отлично справится с такой работой. Если вы на 90 процентов уверены в том, что пользователь каждый раз будет запрашивать определенную фотографию или порцию данных, которые загружаются несколько секунд или требуют сетевого доступа, то заблаговременное выполнение этой работы фоновым потоком будет производить на пользователя ошеломляющий эффект. Существует масса других поводов к тому, чтобы использовать в приложении фоновые потоки для улучшения ответной реакции пользовательского интерфейса в конкретных случаях. Фоновые потоки могут использоваться либо для запуска долговременных алгоритмов в ответ на запрос пользователя, либо для предварительного извлечения данных или выполнения вычислений, исходя из прогнозируемых потребностей пользователя.
Многозадачность и многопоточность в современных операционных системах
Существующие на сегодняшний день современные многозадачные операционные системы позволяют использовать микропроцессор как разделяемый ресурс. Микропроцессорное время распределяется между различными задачами таким образом, что с точки зрения задачи она является единственным владельцем этого ресурса. Это называется многозадачностью, а на выполняемые задачи ссылаются как на процессы. В каждый момент времени на вашем мобильном устройстве выполняются, вероятно, одновременно несколько задач. Скорее всего, количество этих задач больше, чем вы могли бы думать. Некоторые из них обслуживают низкоуровневые потребности, так что "приложениями" вы их даже и не назовете, тогда как другие представляют собой хорошо знакомые вам программы. Время от времени операционная прерывает выполнение задачи в некоторой точке и передает управление другому ожидающему процессу или потоку. Все это хорошо работает, поскольку большую часть времени приложения ничем особенным не заняты; обычно они просто ожидают какого-либо ввода, который необходимо будет обработать. Если же каждый из процессов приложения использует все отведенное для него процессорное время для вычисления значения числа ??!pi□ с бесконечной точностью, то общая производительность значительно страдает. Многозадачность оправдывается тогда, когда значительную часть времени возможности микропроцессора используется в недостаточной степени.
Вопрос о справедливом распределении процессорного времени между различными процессами и потоками – это особая тема, на обсуждение которой потребовалась бы целая книга. Достаточно сказать, что процессорное время распределяется между различными задачами, конкурирующими между собой за обладание этим ресурсом, на основании неких разумных принципов.
Передача управления от одного процесса к другому сопряжена с так называемым переключением контекста. Поскольку каждому процессу приложения кажется, будто он является единственным владельцем микропроцессора, при передаче управления от одного процесса другому должен осуществляться свопинг всех используемых данных. Свопинг охватывает регистры микропроцессора, программный счетчик и указатели виртуальной памяти, а если используется конвейер команд, помещаемых в очередь, или же кэш-память, связанная с микропроцессором, то обработке подлежат и эти данные. Переключение контекста обходится недешево. Чем больше процессов выполняется на устройстве или чем меньше квант времени, выделяемый каждому процессу для выполнения, тем больше доля времени, которое тратится на переключение от одного процесса к другому.
Поскольку обработка операционной системой контекстных переключений между задачами является дорогостоящей, большинство современных систем поддерживают многопоточность, которая представляет собой упрощенную форму многозадачности. Многопоточность обеспечивает поддержку нескольких потоков выполнения внутри одного процесса. Переключение контекста выполнения между различными потоками обходится дешевле, чем переключение контекстов процессов. Однако переключение контекстов потоков также не дается бесплатно. Определенные накладные расходы требуются для изменения исполнительного адреса, свопинга регистровых значений, а также других вспомогательных операций, обеспечивающих гладкое протекание вычислений.
Использование нескольких потоков выполнения в рамках одного и того же пространства памяти приложения может приводить к значительному усложнению кода приложения, обусловленному недетерминированностью времени выполнения вычислений. Попытки двух потоков получить доступ к одним и тем же областям памяти примерно в одно и то же время могут стать причиной возникновения сложных и не до конца определенных ситуаций. Это справедливо как в случае собственного кода С/С++, так и в случае управляемого кода. Для решения проблем подобного рода предназначены блокировки, мьютексы, семафоры и критические разделы; объекты последней разновидности позволяют создавать разделы кода, не являющиеся реентерабельными. В целом, многопоточность напоминает многоярусную автостраду, где есть участки, на которых все движение сливается в одну трассу. И вновь заметим, что подробному рассмотрению всех сложностей и подводных камней параллельного выполнения кода можно было бы посвятить целую книгу.
В конечном счете, все различные процессы и потоки конкурируют между собой за право захватить возможность выполнения на единственном процессоре (или на пуле процессоров, если речь идет о многопроцессорных системах). Каждый процесс может иметь, по крайней мере, один поток, но некоторые процессы могут иметь и несколько потоков выполнения. Операционная система делает все, что возможно, для обеспечения равноправного распределения процессорного времени между процессами и их потоками (примечание: "равноправие" не означает "поровну"), пытаясь минимизировать накладные расходы, связанные с переключением соответствующих контекстов.
Возможность имитации параллельной обработки на машинах с единственным процессором является величайшим благом, но это благо не дается бесплатно. Пользуйтесь им, но пользуйтесь осмотрительно.
Что можно сказать об оборудовании, поддерживающем несколько процессоров?
На многих серверах и некоторых настольных компьютерах устанавливаются несколько микропроцессоров. "Многоядерные" ("multicore") системы с дополнительными микропроцессорами становятся все более распространенными; такие микропроцессоры размещаются на одном кристалле и предоставляют многие из тех преимуществ, которые обеспечиваются наличием нескольких физически независимых процессоров. Многопроцессорные системы обеспечивают возможность подлинно одновременного выполнения кода.
Нет ничего невероятного в том, что в будущем эта тенденция коснется и мобильных устройств. В то время как факторы стоимости и энергопотребления тормозят появление на мобильных устройствах нескольких физических центральных процессоров, возможность использования многоядерных процессоров несколько снижает остроту этих проблем. Тем не менее, эффективное использование мультипроцессоров требует специальной поддержки со стороны операционной системы – задача далеко не тривиальная. До настоящего времени поддержка нескольких микропроцессоров не стояла на повестке дня для большинства операционных систем мобильных устройств; вместо этого указанные операционные системы были ориентированы на компактность и эффективность, а не на возможности параллельного выполнения вычислений на нескольких процессорах.
Тем не менее, иногда задают вопрос: "Если на вычислительном устройстве установлено несколько процессоров, то стоит ли использовать в приложении несколько потоков для ускорения вычислений?" На этот вопрос следует ответить так же, как и при проектировании приложений для однопроцессорных устройств: "Вероятно, не стоит. Использовать несколько потоков следует тогда, когда это делается в интересах асинхронного выполнения некоторых операций". Даже в случае многопроцессорных систем, на которых установлена операционная система, поддерживающая параллельные вычисления, многопоточное выполнение еще не является гарантией лучшей производительности. На то есть две причины:
1. Кроме вашего приложения, операционная система почти всегда выполняет другие процессы, а также заботится о собственных нуждах и осуществляет учет использования ресурсов задачами, например, управляет низкоуровневыми драйверами устройств и планирует задачи. Это означает, что ваше приложение не может рассчитывать на то, что в любой момент времени всеми процессорами, установленными на устройстве, будут распоряжаться только его потоки. Введение дополнительного процесса или потока просто создает "еще один рот", который операционная система, уже и так разделяющая свои вычислительные ресурсы между различными многочисленными задачами, должна накормить. Без получения от операционной системы специального разрешения на выделение процессора определенному потоку выполнения ваше приложение просто создает дополнительный запрос на использование доступных процессорных ресурсов.
2. Практическое проектирование надежных алгоритмов параллельных вычислений представляет собой непростую задачу. Разбиение вычислений на ряд независимых ветвей выполнения и их эффективная координация весьма затруднительны и являются предметом непрекращающихся исследований. Если несколько потоков выполняют различные части одной и той же задачи, то существует значительный риск того, что они где-то пересекутся, пытаясь одновременно получить доступ к одной и той же области памяти или стремясь использовать некоторый ресурс, не допускающий параллельного доступа.
Параллельное выполнение лучше всего применять тогда, когда параллельные задачи не могут мешать друг другу. Именно поэтому серверы нередко только выигрывают от использования нескольких процессоров; серверы часто обслуживают множество независимых и не вступающих между собой в конфликт клиентских запросов. Серверы, располагающие несколькими процессорами, способны параллельно отвечать на многочисленные запросы, одновременно поступающие от разных клиентов. Параллельные задачи, выполнение которых запрашивается, часто связаны только с операциями чтения или изменением данных, которые не перекрываются между собой. Оба вида задач хорошо соответствуют идее параллельных вычислений. Приложения, выполняющиеся на таких серверах, часто располагают пулами потоков, или потоковыми пулами, используемыми для диспетчеризации поступающих запросов.
Приложения с многофункциональными клиентскими пользовательскими интерфейсами обычно не относятся к категории тех, для которых разделение работы на параллельно выполняющиеся части легко осуществить, поскольку такие приложения обычно используются для того, чтобы предоставить возможность одному пользователю работать с набором взаимосвязанных данных и элементами пользовательского интерфейса. Задача приложения, ориентированного на настольные компьютеры, как правило, состоит также в том, чтобы как можно эффективнее реагировать на запросы пользователя, а не служить многим пользователям, соперничающим между собой за разделение процессорного времени между независимыми параллельными задачами.
Если в приложении имеются операции, которые могут выиграть от выполнения в асинхронном режиме, то использование нескольких потоков может оказаться целесообразным независимо от количества установленных процессоров. Основной вопрос заключается в том, стоят ли преимущества асинхронного выполнения тех затрат, с которыми будет связано дополнительное усложнение вашего приложения. Наличие нескольких процессоров на клиентских вычислительных устройствах означает наличие дополнительных рабочих рук, что способствует повышению общей производительности устройства. В случае "толстых" клиентов рассматривать эти процессоры как выделенные ресурсы, которые могут назначаться различным частям вашего приложения, имеет смысл лишь в редких случаях.
В каких случаях следует использовать фоновые потоки
Вообще говоря, один поток в вашем приложении должен быть основным. Этот поток должен управлять пользовательским интерфейсом приложения, а когда все активные окна закроются, приложение должно завершить выполнение. При завершении приложения обычно требуется уведомить все выполняющиеся фоновые потоки о необходимости прекратить выполнение, и когда не останется ни одного открытого окна приложения, которое требовало бы поддержки со стороны основного потока и оставляло его активным, он может прекратить свое выполнение. (В этот момент осуществляется выход из функции main() приложения и управление передается обратно среде времени выполнения и операционной системе для окончательного освобождения памяти от данных приложения.) Наличие нескольких потоков, управляющих собственными окнами пользовательского интерфейса, существенно усложняет описанную модель, и этого следует избегать.
Иногда для выполнения некоторых операций, запрошенных пользователем, может требоваться значительное или неопределенное время. Если это время исчисляется несколькими секундами, то, вероятно, целесообразно вывести на экран курсор ожидания и выполнять операцию в синхронном режиме с использованием потока пользовательского интерфейса. Если же для этого требуется большее или неопределенное время, то подходящим решением будет передача выполнения этой работы фоновому потоку. Это может быть сделано двумя способами:
1. Создайте новый поток. В этой модели запускается новый поток, в качестве точки входа которого указывается нужная функция. Функция начинает выполняться, а по ее завершении выполнение потока прекращается.
2. Воспользуйтесь ожидающим потоком. В этой модели фоновый поток или пул потоков создаются заблаговременно и ждут, пока для них не найдется работа. В типичных случаях фоновые потоки находятся в состоянии ожидания или в блокированном состоянии и ожидают, пока их не пробудит некоторое действие, или периодически пробуждаются в соответствии с сигналами соответствующего таймера. Пробужденный поток выполняет затребованную работу, после чего вновь засыпает в ожидании очередных запросов, которые могут вновь его пробудить.
НА ЗАМЕТКУ
В версии NET Framework для настольных компьютеров имеется встроенная поддержка потоковых пулов. В этой модели для передачи выполнения работы ожидающему потоковому пулу используются "асинхронные делегаты". В версии 1 1 .NET Compact Framework поддержка универсальных асинхронных делегатов отсутствует.
Программисты на С/С++ могут рассматривать делегаты как аналоги указателей на функции. Программисты на LISP могут считать их аналогичными оболочкам. Делегаты позволяют задать привязку к методу определенного объекта и впоследствии вызвать этот метод, не ссылаясь на объект или имя конкретного метода. .NET Compact Framework обеспечивает поддержку делегатов. Асинхронные делегаты позволяют выполнять в асинхронном режиме методы, с которыми они связаны, с использованием потока из пула фоновых потоков. Указанный потоковый пул управляется средой времени выполнения. Асинхронные делегаты являются превосходным средством абстрагирования, поскольку освобождают разработчика от необходимости самостоятельного проектирования и тестирования собственных механизмов управления потоковыми пулами. Поскольку .NET Compact Framework с самого начала предназначалась для выполнения на устройствах с ограниченными ресурсами, взаимодействие между потоками для передачи параметров, что требуется при использовании асинхронных делегатов общего назначения, проектным решением для версии 1.1 не предусматривалось. Если вы хотите поддерживать потоковый пул, используя .NET Compact Framework, и выполнять фоновые задачи с использованием управляемых потоков, то можете это осуществить путем явного вызова метода System.Threading.ThreadPool.QueueUserWorkItem().
Вместо поддержки универсальных асинхронных делегатов в NET Compact Framework предусмотрена встроенная поддержка выполнения некоторых часто запрашиваемых задач в асинхронном режиме. В отношении таких задач, как создание HTTP-запроса данных с Web– сервера, поддержка асинхронного режима в .NET Compact Framework и .NET Framework совпадает. Кроме того, поддерживается класс System.Threading.Timer, обеспечивающий выполнение делегатов таймера фоновыми потоками. (Управляет этими потоками среда времени выполнения.) Таким образом, несмотря на то. что универсальные асинхронные делегаты в версии 1.1 NET Compact Framework не поддерживаются, в этой версии реализована поддержка конкретных асинхронных вызовов для большинства наиболее распространенных задач.
Создать новый поток для выполнения фоновой обработки или использовать встроенную поддержку потокового пула значительно проще, чем спроектировать собственный нестандартный потоковый пул и организовать управление им. С использования этих подходов я и рекомендовал бы вам начинать, если требуется фоновая обработка. Единственный недостаток явного создания нового потока для выполнения фоновой задачи – это дополнительные накладные расходы, обусловленные созданием и разрушением потоков по запросу. В большинстве случае этот эффект будет пренебрежимо малым, но простота решения делает этот выбор оправданным. Лишь в тех случаях, когда вы твердо уверены в том, что подход, основанный на создании потоков только тогда, когда это действительно необходимо, не в состоянии удовлетворить ваши запросы, следует проанализировать возможность применения подхода, связанного с использованием собственного потокового пула. Начинайте с самого простого и привлекайте более сложные средства лишь тогда, когда необходимость этого для вас является совершенно очевидной.







