• Введение: когда и как следует использовать многопоточное выполнение
  • Многозадачность и многопоточность в современных операционных системах
  • В каких случаях следует использовать фоновые потоки
  • Рекомендации по использованию потоков в мобильных приложениях 
  • Назначайте обслуживание пользовательского интерфейса основному потоку
  • Стремитесь поддерживать способность пользовательского интерфейса к отклику на высоком уровне
  • Начинайте с создания однопоточного приложения
  • В простых случаях пытайтесь обойтись без многопоточного выполнения, используя курсоры ожидания
  • Рассмотрите возможность использования фоновых потоков, если выполнение задачи требует длительного или неопределенного времени
  • Максимально упрощайте многопоточный код и документируйте его для повышения надежности
  • Рассмотрите возможность предварительного выполнения некоторой работы, осуществляемой кодом
  • Пример использования фонового потока для выполнения отдельной задачи
  • Потоки и пользовательский интерфейс
  • Пример использования фоновой обработки одновременно с обновлением данных высокоприоритетного потока пользовательского интерфейса
  • Резюме 
  • ГЛАВА 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 не поддерживаются, в этой версии реализована поддержка конкретных асинхронных вызовов для большинства наиболее распространенных задач.

    Создать новый поток для выполнения фоновой обработки или использовать встроенную поддержку потокового пула значительно проще, чем спроектировать собственный нестандартный потоковый пул и организовать управление им. С использования этих подходов я и рекомендовал бы вам начинать, если требуется фоновая обработка. Единственный недостаток явного создания нового потока для выполнения фоновой задачи — это дополнительные накладные расходы, обусловленные созданием и разрушением потоков по запросу. В большинстве случае этот эффект будет пренебрежимо малым, но простота решения делает этот выбор оправданным. Лишь в тех случаях, когда вы твердо уверены в том, что подход, основанный на создании потоков только тогда, когда это действительно необходимо, не в состоянии удовлетворить ваши запросы, следует проанализировать возможность применения подхода, связанного с использованием собственного потокового пула. Начинайте с самого простого и привлекайте более сложные средства лишь тогда, когда необходимость этого для вас является совершенно очевидной.

    Рекомендации по использованию потоков в мобильных приложениях 

    Назначайте обслуживание пользовательского интерфейса основному потоку

    Как ранее уже отмечалось в этой главе, в образном представлении основной поток — это все равно, что портье в хорошей гостинице. Самое главное, чтобы портье всегда находился на своем рабочем месте, оказывая помощь посетителям гостиницы в ответ на их обращения. Когда портье о чем-то просят, он может выполнить эту просьбу самостоятельно, если работа не займет много времени. Задачи, для решения которых требуется длительное время, портье может перепоручать другим сотрудникам гостиницы, чтобы самому иметь возможность незамедлительно обслужить следующего посетителя. Аналогичную модель вы должны использовать и в проекте своего приложения, где в роли портье выступает пользовательский интерфейс приложения, а в роли остальных сотрудников — фоновые потоки.

    Стремитесь поддерживать способность пользовательского интерфейса к отклику на высоком уровне

    Вашей самой главной проектной задачей должно быть поддержание постоянной готовности пользовательского интерфейса к отклику и интерактивному взаимодействию с пользователем. Это означает, что вы не должны допускать, чтобы пользовательский интерфейс надолго стопорился, и прилагать все усилия к тому, чтобы пользователь всегда был информирован о том, что происходит с его запросом, и имел уверенность в том, что он способен влиять на процесс выполнения фоновых задач.

    Начинайте с создания однопоточного приложения

    Поскольку введение дополнительных потоков значительно усложняет приложение, используйте их только тогда, когда это диктуется вескими причинами. Старайтесь не поддаваться "очарованию потоками".

    В простых случаях пытайтесь обойтись без многопоточного выполнения, используя курсоры ожидания

    Курсоры ожидания — это вариант многопоточности "для бедных", который, тем не менее, обладает множеством достоинств, основным из которых является его простота. В этом случае полезная работа не передается фоновому потоку для выполнения в асинхронном режиме, однако, как бы в качестве вежливого извинения перед пользователем за то, что приложение все еще выполняет порученную задачу и ее завершения придется немного подождать, отображается курсор ожидания.

    Если вам предварительно известно, в каких местах программы происходит задержка, предусмотрите вывод на экран курсора ожидания, появление которого укажет пользователю на то, что приложение выполняется, а исчезновение — на восстановление способности приложения к интерактивному взаимодействию. Вашей первой линией обороны в плане обеспечения добротного пользовательского интерфейса должно быть уведомление пользователя о том, что в течение ближайшего времени интерфейс не будет реагировать на его запросы. Отображение курсора ожидания — это неплохой способ информирования пользователей о том, что работа выполняется и вскоре будет завершена. 

    Рассмотрите возможность использования фоновых потоков, если выполнение задачи требует длительного или неопределенного времени

    Подход, основанный на использовании курсора ожидания, является не совсем уместным, если либо 1) ожидаемая продолжительность выполнения задачи настолько велика, что длительное отображение этого курсора лишь усилит раздражение пользователя, либо 2) длительность выполнения задачи неизвестна или не ограничена, что бывает при доступе к внешним ресурсам устройства. В обоих случаях проанализируйте возможность использования фоновых потоков. 

    Максимально упрощайте многопоточный код и документируйте его для повышения надежности

    Безопасность многопоточной поддержки — штука хитрая. Если не уделить должное внимание тому, как осуществляется считывание и запись переменных-членов, то может случиться так, что ваше приложение будет пытаться прочитать в одном потоке переменную, запись которой была начата другим потоком, но еще не успела закончиться; "атомарность", то есть неделимость — выполнение за один раз от начала до конца, для большинства операций над данными, находящимися в памяти, не гарантируется, поскольку для записи большинства типов данных требуется выполнение нескольких инструкций микропроцессора. Тот факт, что возникновение проблем подобного рода зависит от временных характеристик выполнения потоков и случается редко, значительно затрудняет их обнаружение, воспроизведение и отладку. Даже ecли гарантирована атомарность доступа к переменным, но при этом было уделено недостаточное внимание тому, как осуществляются вызовы функций-членов классов, то вы можете оказаться в ситуации, когда либо портятся данные, либо программа ведет себя непредсказуемым образом, поскольку соответствующие данные параллельно изменяются алгоритмами, выполняющимися разными потоками; представьте, например два потока, которые одновременно пытаются вставлять и удалять записи в одном и том же связанном списке. Для надежной обработки таких ситуаций необходимо определить "критические разделы" кода; тем самым будет гарантироваться, что любой код, связанный с одним и тем же объектом семафора, сможет выполнять только одним потоком. (В C# это достигается за счет использования оператора lock(объект), а в Visual Basic — с использованием оператора SyncLock(объект). Для получения более подробной информации относительно двух указанных операторов обратитесь к библиотеке справочной документации MSDN.) Ситуацию могут еще более осложнять "зависания", или "взаимоблокировки", когда два потока, выполняющиеся в одно и то же время в разных критических разделах, вызывают код, который должен войти в критический раздел, "принадлежащий" в данный момент другому потоку; при вхождении в критический раздел другого потока будет приостановлено выполнение каждого потока. По этой причине, а также с учетом факторов производительности, чрезмерно вольное использование критических разделов может привести к появлению дополнительных проблем.

    Вы могли бы попытаться сделать все свойства и методы своих классов безопасными в отношении многопоточного выполнения, однако осуществить это чрезвычайно трудно с технической точки зрения и расточительно с точки зрения производительности. В конце концов, весь код вашего приложения оказался бы испещренным множеством самых различных критических разделов и бесчисленным количеством всевозможных объектов, используемых в качестве семафоров совместно с критическими разделами. Код такого типа чрезвычайно трудно проектировать и тестировать; кроме того, он характеризуется повышенными накладными расходами, обусловленными необходимостью осуществления проверок, обеспечивающих безопасность многопоточности, и чрезмерно сериализованным выполнением. Ни в .NET Framework, ни в .NET Compact Framework попытки решения этой задачи не делаются; вместо этого в обеих средах используется подход, основанный на тщательном документировании всех возможностей, и явное объявление того, какие операции безопасны в отношении многопоточного выполнения, а какие таковыми не являются. Предполагается, что разработчики внимательно ознакомятся с документацией и будут ею руководствоваться при использовании классов, свойств и методов. Метод класса, не являющийся безопасным в указанном смысле, не должен вызываться для параллельного выполнения из других потоков. Вместо этого, следует либо создать два различных экземпляра класса, либо сериализовать вызов не являющегося безопасным метода, поместив его в критический раздел. Именно таким способом обеспечивается доступ ко всему, что является необходимым и безопасным, а что таковым не является — документируется.

    Аналогичный подход вам следует использовать и в своих проектах. Вы должны явно указывать в коде классы, функции и свойства, требующие доступа из нескольких потоков, и сводить их количество к минимуму. Критические разделы кода следует объявлять и использовать лишь в тех случаях, когда многопоточный доступ является абсолютно необходимым, и обнаружены проблемы параллельного выполнения или доступа к данным, устранить которые простыми методами не удается. Очень тщательно проектируйте, программируйте и тестируйте эти специальные классы и функции и столь же тщательно документируйте все свои классы, свойства и методы. Если многопоточный доступ к типу, свойству или методу не является безопасным или у вас есть сомнения относительно безопасности доступа к ним из нескольких потоков, документируйте это в своем коде. Например:

    // К ДАННОЙ <ПЕРЕМЕННОЙ/СВОЙСТВУ/МЕТОДУ> ДОСТУП ИЗ НЕСКОЛЬКИХ ПОТОКОВ

    // ОСУЩЕСТВЛЯТЬСЯ НЕ ДОЛЖЕН!!!

    // Предполагается, что этот метод будет использоваться

    // <высокоприоритетным/фоновым> потоком для ...

    Определение тех критических участков кода, которые должны быть безопасными в отношении многопоточного выполнения, и явное обозначение тех участков кода, которые либо не являются безопасными в указанном смысле, либо проектировались без учета этого фактора, позволяет вам сосредоточить все свои усилия на тех участках кода, к которым действительно требуется безопасный параллельный доступ из нескольких потоков. Кроме того, явное документирование этих аспектов кода позволит вам быть уверенным в том, что он сохранит свою надежность в процессе дальнейшей разработки и сопровождения.

    Рассмотрите возможность предварительного выполнения некоторой работы, осуществляемой кодом

    Для обработки некоторых пользовательских запросов требуется довольно большое время. В качестве примера можно привести расчет или загрузку значительного количества данных, выполнение сетевого запроса или визуализацию сложных изображений в ответ на поступающие запросы. Если выполнение этих операций тормозит работу приложения, то вынужденное ожидание будет раздражать пользователя. Вы должны приложить все усилия к тому, чтобы такие ситуации не возникали. Если ваше приложение допускает возможность достаточно надежного вероятностного прогнозирования очередных действий пользователя и при этом достаточно хорошо известно, какие виды трудоемкой или длительной обработки данных могут потребоваться в ответ на эти действия, то целесообразно подумать над тем, как выполнить эту работу заблаговременно, не дожидаясь соответствующего пользовательского запроса. В качестве образной аналогии представьте себя владельцем ресторана, постоянный посетитель которого ежедневно приходит к 8 часам утра и, торопясь, заказывает яичницу из двух яиц, пирог с черникой, тарелку каши и чашку кофе. Нетрудно догадаться, что в этом случае имело бы смысл ежедневно подготавливать эти блюда к подаче в указанное время. Даже если посетитель и не придет в какой-то из дней, эта мера все равно будет оправданной, поскольку значительно повысит удовлетворенность клиента качеством обслуживания в те дни, которые им не были пропущены. Заблаговременно готовясь к немедленному оказанию услуг, как только они будут затребованы, вы сможете обеспечить уникальное высококачественное обслуживание клиентов.

    Выполнение части работы с упреждением, исходя из ожидаемых запросов клиента, создает хорошие предпосылки для устранения или существенного сокращения длительности раздражающих простоев, с которыми пользователю приходится сталкиваться в процессе использования вашего мобильного приложения. При правильной реализации такого подхода он может существенно повысить удовлетворенность пользователей мобильных устройств качеством услуг, предоставляемых им приложением, в немедленном получении которых они хотят быть уверенными.

    Подобно тому, как не следует чрезмерно "очаровываться многопоточностью", очень важно иметь твердую уверенность в том, что заблаговременная обработка данных действительно необходима приложению, прежде чем браться за проектирование подобного рода систем. Заблаговременная обработка может значительно усложнять приложение, и ее следует использовать лишь в тех случаях, когда это окупается повышением комфортности условий работы пользователя. Наилучший способ проверить, так ли это, состоит в проведении точных измерений длительности задержек, с которыми будут сталкиваться пользователи, а также тестировании прогнозируемых результатов заблаговременной обработки данных.

    Заблаговременное прогнозирование потребностей пользователя

    Заблаговременное выполнение части работы, в которой заинтересован пользователь, может требовать от вас определенного мастерства, но и положительный эффект от этого может оказаться значительным. Если этот подход реализован так, как следует, то пользователям почти никогда не представится случая по достоинству оценить, какая громадная работа для этого вами проделана; точно так же здоровый человек почти никогда не обращает внимания на то, как работает его сердце.

    В качестве хорошего примера можно привести программу для просмотра изображений в Windows XP. Окно этой программы появляется сразу же после того, как вы дважды щелкнете мышью на имени файла изображения в окне проводника. В результате этого изображение загружается и отображается на экране. Одновременно с этим, но незаметно для пользователя и без какой-либо инициативы с его стороны, после вывода на экран первого изображения на фоне загружается также изображение, файл которого размещен в каталоге вслед за первым. Если следующим действием пользователя, которое можно считать наиболее вероятным, будет щелчок на кнопке Next Image (Следующее изображение) в программе для просмотра изображений, то следующая фотография незамедлительно появится на экране без видимой задержки. В случае современных крупных цифровых фотографий, загрузка, распаковка и масштабирование которых требуют значительного времени, это не такое уж и малое достижение. Описанная заблаговременная загрузка производится в основном только для изображений, которые располагаются следующими в списке файлов каталога изображений. Если вы щелкнете на кнопке Previous Image (Предыдущее изображение), то, вероятнее всего, увидите на экране сообщение "Generating preview" ("Генерируется изображение предварительного просмотра"), появляющееся на короткое время на экране при загрузке фотографии по требованию. Переходы в обратном направлении используются менее часто и поэтому не оптимизируются. Поскольку для цифровых изображений могут требоваться значительные ресурсы памяти, осуществление предварительной загрузки большого количества изображений в предвидении того, что пользователь может захотеть их просмотреть, на сегодняшний день вызывает затруднения. Аналогичным образом, то же самое сообщение "Generating preview" будет появляться на экране, если вы выполните ряд быстрых последовательных щелчков на кнопке Next Image, требующих загрузки очередных изображений; это объясняется тем, что вы требуете большего, чем позволяют возможности средств опережающего просмотра в отношении загрузки следующих фотографий. В большинстве случаев, прежде чем переходить к следующей фотографии, текущую фотографию рассматривают хотя бы в течение одной-двух секунд, и именно для такого сценария и была предусмотрена оптимизация.

    Оптимизация загрузки изображений программой просмотра основывается на вполне разумных предположениях:

    1. В большинстве случае порядок просмотра фотографий соответствует продвижению по списку в прямом направлении.

    2. Обычно каждую фотографию рассматривают достаточно долго для того, чтобы система успела выполнить фоновую загрузку следующей фотографии.

    Независимо от того, используется ли в программе просмотра изображений Windows XP фоновый поток для решения этой задачи или не используется (мне это неизвестно), она демонстрирует пример разумного применения асинхронной обработки для оптимизации рабочего процесса в случае наиболее распространенных действий. Прекрасная работа!

    Пример использования фонового потока для выполнения отдельной задачи

    Показанный в листинге 9.1 код представляет класс, который позволяет управлять выполнением задачи в фоновом потоке. Для отслеживания состояний подготовки приложения к выполнению, запуска нового потока, выполнения кода потоком и выхода из потока выполнения по завершении работы применяется конечный автомат.

    Кроме того, в рассматриваемом примере основному потоку предоставляется возможность запрашивать прекращение выполнения фоновой задачи. Для уведомления потока, выполняющего фоновую задачу, о поступлении запроса на прекращение выполнения, используется вызов метода m_threadExecute.setProcessingState(ThreadExecuteTask.ProcessingState.requestAbort) из другого потока. За периодическую проверку этого состояния и осуществление возможного прекращения выполнения операции отвечает код, выполняемый фоновым потоком. Конечный автомат для класса ThreadExecuteTask представлен на рис. 9.1.

    Рис. 9.1. Конечный автомат для отдельной задачи, выполняемой фоновым потоком


    Листинг 9.1. Код для управления выполнением одиночной задачи фоновым потоком

    using System;
     

    public class ThreadExecuteTask {

     //Перечисляем возможные состояния

     public enum ProcessingState {

      //-------------------

      //Начальное состояние

      //-------------------

      //Пока ничего интересного не происходит

      notYetStarted,

      //-----------------

      //Рабочие состояния

      //-----------------

      //Ожидание запуска фонового потока

      waitingToStartAsync,

      //Выполнение кода в фоновом потоке

      running,

      //Запросить отмену выполнения вычислений

      requestAbort,

      //--------------------

      //Состояния завершения

      //--------------------

      //Состояние завершения: выполнение фонового потока

      //успешно завершено

      done,

      //Состояние завершения: выполнение потока отменено

      //до его завершения

      aborted

     }

     ProcessingState m_processingState;

     public delegate void ExecuteMeOnAnotherThread(ThreadExecuteTask checkForAborts);

     private ExecuteMeOnAnotherThread m_CallFunction;

     private object m_useForStateMachineLock;


     public ThreadExecuteTask(ExecuteMeOnAnotherThread functionToCall) {

      //Создать объект, который мы можем использовать

      //в конечном автомате в целях блокировки

      m_useForStateMachineLock = new Object();


      //Обозначить готовность к началу выполнения

      m_processingState = ProcessingState.notYetStarted;


      //Сохранить функцию, которую необходимо вызвать

      //в новом потоке

      m_CallFunction = functionToCall;


      //----------------------------------------------------------

      //Создать новый поток и вызвать в нем функцию на выполнение:

      // this.ThreadStartPoint()

      //----------------------------------------------------------

      System.Threading.ThreadStart threadStart;

      threadStart = new System.Threading.ThreadStart(ThreadStartPoint);


      System.Threading.Thread newThread;

      newThread = new System.Threading.Thread(threadStart);


      //Обозначить готовность к началу выполнения (в целях определенности

      //это важно сделать еще до того, как будет запущен поток!)

      setProcessingState(ProcessingState.waitingToStartAsync);


      //Дать ОС команду начать выполнение нового потока в асинхронном режиме

      newThread.Start();


      //Возвратить управление функции, вызывающей этот поток

     }


     //---------------------------------------------

     //Эта функция является точкой входа, вызываемой

     //для выполнения в новом потоке

     //---------------------------------------------

     private void ThreadStartPoint() {

      //Установить состояние обработки, соответствующее

      //выполнению функции в новом потоке!

      setProcessingState(ProcessingState.running);

      //Запустить на выполнение пользовательский код и передать указатель в

      //наш класс, чтобы этот код мог периодически проверять, не поступил ли

      //запрос на прекращение выполнения

      m_CallFunction(this);


      //Если выполнение не было отменено, изменить состояние таким образом,

      //чтобы оно соответствовало успешному завершению

      if (m_processingState != ProcessingState.aborted) {

       //Обозначить завершение выполнения

       setProcessingState(ProcessingState.done);

      }

      //Выйти из потока...

     }


     //----------------

     //Конечный автомат

     //----------------

     public void setProcessingState(ProcessingState nextState) {

      //B любой момент времени только одному потоку выполнения

      //могут быть разрешены попытки изменить состояние

      lock(m_useForStateMachineLock) {

       //B случае попытки повторного вхождения в текущее состояние

       //никакие дополнительные действия не выполняются

       if (m_processingState == nextState) {

        return;

       }

       //------------------------------------------------------

       //Простейший защитный код, гарантирующий

       //невозможность перехода в другое состояние, если задача

       //либо успешно завершена, либо успешно отменена

       //------------------------------------------------------

       if ((m_processingState == ProcessingState.aborted) ||

           (m_processingState == ProcessingState.done)) {

        return;

       }

       //Убедиться в допустимости данного изменения состояния

       switch (nextState) {

       case ProcessingState.notYetStarted:

        throw new Exception("Переход в состояние 'notYetStarted' невозможен");

       case ProcessingState.waitingToStartAsync:

        if (m_processingState != ProcessingState.notYetStarted) {

         throw new Exception("Недопустимое изменение состояния");

        }

        break;

       case ProcessingState.running:

        if (m_processingState != ProcessingState.waitingToStartAsync) {

         throw new Exception("Недопустимое изменение состояния");

        }

        break;

       case ProcessingState.done:

        //Мы можем завершить работу лишь тогда, когда она выполняется.

        //Это возможно также в тех случаях, когда пользователь затребовал

        //отмену выполнения, но работа к этому моменту уже была закончена

        if ((m_processingState != ProcessingState.running) &&

           (m_processingState != ProcessingState.requestAbort)) {

         throw new Exception("Недопустимое изменение состояния");

        }

        break;

       case ProcessingState.aborted:

        if (m_processingState != ProcessingState.requestAbort) {

         throw new Exception("Недопустимое изменение состояния");

        }

        break;

       }

       //Разрешить изменение состояния

       m_processingState = nextState;

      }

     }


     public ProcessingState State {

      get {

       ProcessingState currentState;

       //Предотвратить попытки одновременного чтения/записи состояния

       lock(m_useForStateMachineLock) {

        currentState = m_processingState;

       }

       return currentState;

      }

     }

    } //Конец класса

    В листинге 9.2 представлен код, имитирующий выполнение работы фоновым потоком. Когда фоновый поток начинает выполнять код, на экране отображается окно сообщения. Выполнение работы имитируется созданием серии пауз длительностью в одну треть секунды, в промежутках между которыми рабочий код проверяет, не поступил ли от другого потока запрос на прекращение выполнения.

    Листинг 9.2. Тестовая программа для выполнения работы в фоновом потоке

    using System;

    //-------------------------------------------------

    //Тестовый код, который используется для выполнения

    //фоновым потоком

    //-------------------------------------------------

    public class Test1 {

     public int m_loopX;

     //------------------------------------------------------------------

     //Функция, вызываемая фоновым потоком

     // [in] threadExecute: Класс, управляющий выполнением нашего потока.

     // Мы можем контролировать его для проверки

     // того, не следует ли прекратить вычисления

     //------------------------------------------------------------------

     public void ThreadEntryPoint(ThreadExecuteTask threadExecute) {

      //Это окно сообщений будет отображаться в контексте того потока,

      //в котором выполняется задача

      System.Windows.Forms.MessageBox.Show("Выполнение ТЕСТОВОГО ПОТОКА");

      //------

      //60 раз

      //------

      for (m_loopX = 0; m_loopX < 60; m_loopX++) {

       //Если затребована отмена выполнения, мы должны завершить задачу

       if (threadExecute.State == ThreadExecuteTask.ProcessingState.requestAbort) {

        threadExecute.setProcessingState(ThreadExecuteTask.ProcessingState.aborted);

        return;

       }

       //Имитировать выполнение работы: пауза 1/3 секунды

       System.Threading.Thread.Sleep(333);

      }

     }

    } //Конец класса

    В листинге 9.3 содержится код, который можно запустить на выполнение из основного потока пользовательского интерфейса с целью активизации и контроля фоновой обработки. Этот код не является независимым классом и должен помещаться в форму, с кнопками которой должны быть связаны события щелчков на кнопках.

    Листинг 9.3. Код для запуска и тестирования приведенного выше тестового кода

    //Класс, который будет управлять выполнением нового потока

    private ThreadExecuteTask m_threadExecute;


    //Класс, метод которого мы хотим выполнять в асинхронном режиме

    Test1 m_testMe;


    //----------------------------------------------------------

    //Этот код должен быть запущен ранее другого кода, поскольку

    //он запускает новый поток выполнения!

    //

    //Создать новый поток и обеспечить его выполнение

    //----------------------------------------------------------

    private void buttonStartAsyncExecution_Click(object sender, System.EventArgs e) {

     //Создать экземпляр класса, метод которого мы хотим вызвать

     //в другом потоке

     m_testMe = new Test1();


     //Упаковать точку входа метода класса в делегат

     ThreadExecuteTask.ExecuteMeOnAnotherThread delegateCallCode;

     delegateCallCode = new ThreadExecuteTask.ExecuteMeOnAnotherThread(m_testMe.ThreadEntryPoint);


     //Дать команду начать выполнение потока!

     m_threadExecute = new ThreadExecuteTask(delegateCallCode);

    }


    //Проверить состояние выполнения

    private void buttonCheckStatus_Click(object sender, System.EventArgs e) {

     //Запросить у класса управления потоком, в каком состоянии он находится

     System.Windows.Forms.MessageBox.Show(m_threadExecute.State.ToString());

     //Запросить класс, метод которого выполняется в потоке,

     //о состоянии выполнения

     System.Windows.Forms.MessageBox.Show(m_testMe.m_loopX.ToString());

    }


    //Принудительно вызвать запрещенное изменение состояния

    //(это приведет к возбуждению исключения)

    private void buttonCauseException_Click(object sender, System.EventArgs e) {

     m_threadExecute.setProcessingState(ThreadExecuteTask.ProcessingState.notYetStarted);

    }


    //Послать асинхронному коду запрос с требованием отмены его выполнения

    private void buttonAbort_Click(object sender, System.EventArgs e) {

     m_threadExecute.setProcessingState(ThreadExecuteTask.ProcessingState.requesAbort);

    }

    Потоки и пользовательский интерфейс

    Время от времени приходится слышать вопрос: "Следует ли использовать несколько потоков для выполнения пользовательского интерфейса?" На этот вопрос почти однозначно должен быть дан отрицательный ответ. Привлекать для управления различными частями пользовательского интерфейса несколько потоков практически никогда не имеет смысла. Это особенно относится к мобильным устройствам, в которых пользовательские интерфейсы приложения, как правило, занимают весь экран.

    Обычно окна связываются с потоком, который является их владельцем; это справедливо для операционных систем Windows СЕ, Pocket PC и Microsoft Smartphone (а также для таких настольных операционных систем, как Windows XP и более ранние версии Windows). Для каждого окна имеется поток, которому оно принадлежит и который им управляет. Один и тот же поток может владеть несколькими окнами. Поток играет роль "генератора сообщений" по отношению к этим окнам и пересылает им сообщения, когда окно должно быть перерисовано, когда нажимается клавиша, когда выполняется щелчок на кнопке и так далее.

    Хотя и можно организовать приложение таким образом, чтобы пользовательский интерфейс обслуживался несколькими потоками (например, по одному потоку на одно окно верхнего уровня), это почти никогда не принесет никакой пользы. Это только усложняет структуру приложения, ничуть не ускоряя его работу. Если вы считаете, что для пользовательского интерфейса необходимо задействовать несколько потоков, задайте себе вопрос, а для чего это в действительности вам надо и нельзя ли при этом использовать один основной поток пользовательского интерфейса и несколько рабочих фоновых потоков, что сделает модель гораздо более понятной.

    В подавляющем большинстве случаев намерением использовать несколько потоков, каждый из которых связан с пользовательским интерфейсом, движет желание обеспечить постоянную информированность пользователя о состоянии задач, выполняемых различными фоновыми потоками. Гораздо лучше периодически запрашивать эти данные при помощи одного высокоприоритетного потока интерфейса, используя таймер, чем управлять несколькими окнами на экране посредством нескольких потоков. Проектирование фоновых задач в виде классов, использующих конечные автоматы, упрощает получение соответствующих данных от потока пользовательского интерфейса. Вот вам еще один веский довод в пользу применения подходов, основанных на использовании конечных автоматов.

    Даже если ваша оконная модель и не связана с какими-либо специфическими потоками, обычно целесообразнее иметь только один поток выполнения, который "берет на себя попечительство" над пользовательским интерфейсом. 

    Работая в .NET Compact Framework, не пытайтесь получить доступ к элементам управления пользовательского интерфейса из потоков, которым они не принадлежат

    Ни вариант .NET Framework для настольных компьютеров, ни вариант .NET Compact Framework для мобильных устройств не поддерживают доступ к большинству свойств и методов элементов пользовательского интерфейса из потоков, которым они не принадлежат. Хотя такой код и будет нормально компилироваться, результаты его выполнения будут непредсказуемыми. Для обеспечения межпоточных вызовов в .NET Framework и .NET Compact Framework поддерживается метод Control.Invoke(). В версии 1.1 .NET Compact Framework поддерживается лишь использование механизма Control.Invoke() для вызова функций без параметров. Более подробную информацию относительно применения этого метода вы найдете в справочной документации MSDN. Не составляет труда организовать на приемлемом уровне обмен данными между фоновым потоком и потоком пользовательского интерфейса, предусмотрев для этого выполняющийся в потоке пользовательского интерфейса код, который периодически опрашивает объект, специально предназначенный для управления выполнением фоновых потоков, с целью определения того, имеются ли данные, ожидающие реакции пользовательского интерфейса. Обычно сделать это гораздо проще, чем погружаться во все тонкости межпоточного вызова методов.

    Второй подход связан с использованием косвенно вызываемого делегата, указывающего на функцию формы вашего приложения. В качестве такой функции (не имеющей параметров — см. выше) может быть назначена функция класса формы, которую можно вызвать посредством метода Invoke() формы. Вызов Invoke() приведет к выполнению упомянутой функции в потоке пользовательского интерфейса. После этого функция может извлечь любые необходимые данные и соответствующим образом обновить пользовательский интерфейс. Достоинством такого подхода является то, что высокоприоритетный поток не должен выполнять цикл опроса и получает обновленную информацию сразу же после завершения фоновой работы. Недостаток этого подхода заключается в том, что он требует создания (предположительно, на очень короткое время) синхронной связи между фоновым и высокоприоритетным потоками выполнения. Когда делегат запускается на выполнение в фоновом потоке, выполнение фонового потока приостанавливается, контекст выполнения переключается на высокоприоритетный поток, и делегат начинает выполняться. Это препятствует переключению фонового потока на выполнение другой работы, находящейся в очереди. Выполнение фонового потока сможет возобновиться лишь после того, как выполнение делегата завершится.

    Пример использования фоновой обработки одновременно с обновлением данных высокоприоритетного потока пользовательского интерфейса

    Сейчас мы вернемся к нашему примеру с простыми числами, который рассматривался в главе 5, заметно изменив и усовершенствовав его. На этот раз мы создадим приложение типа SmartPhone, вычисляющее простые числа большой величины. Во время проведения большого объема вычислений фоновым потоком приложение будет сохранять способность к интерактивному взаимодействию с пользователем. Приложение предоставляет пользователю возможность при необходимости прекратить выполнение фонового потока. Кроме того, и это немаловажно, в приложении реализован неплохой способ информирования пользователя о состоянии выполнения интересующей его задачи. Зная, что фоновая задача успешно выполняется, пользователь будет чувствовать себя более комфортно

    Данное приложение несложно адаптировать для выполнения на Pocket PC. Наш выбор Microsoft Smartphone в качестве целевой платформы был сделан исключительно в интересах разнообразия.

    Рис. 9.2. Окно интегрированной среды разработки Visual Studio, предcтавляющее проектируемый пользовательский интерфейс приложения типа Smartphone


    Рис. 9.3. Экранные снимки эмулятора Smartphone, полученные в процессе вычисления приложением простых чисел

    НА ЗАМЕТКУ

    Если вы используете Visual Studio .NET 2003, то вам необходимо загрузить SDK для Windows Mobile 2003-based Smartphones. Visual Studio NET 2003 поставлялась с "коробочным" вариантом средств разработки приложений для Pocket PC, но не для Smartphone. Поскольку SDK для Smartphone поставлялся после выхода Visual Studio .NET 2003, его следует загрузить и установить поверх Visual Studio .NET. Пакет SDK можно бесплатно загрузить с Web- сайта компании Microsoft (см. приложение А). Этот SDK включает в себя компоненты, необходимые для проектирования пользовательских интерфейсов Smartphone, а также эмулятор Smartphone, позволяющий выполнять приложения, даже если вы не располагаете физическим устройством Smartphone.

    Чтобы создать и запустить указанное приложение, выполните следующие действия:

    1. Запустите Visual Studio .NET (2003 или более позднюю версию) и создайте проект C# Smart Device Application.

    2. Выберите в качестве целевой платформы Smartphone. (Для вас будет автоматически создан проект, и на экране появится конструктор форм для Smartphone.)

    3. Используя рис. 9.2 в качестве образца для компоновки формы, добавьте в нее следующие элементы управления:

     • TextBox (textBox1); задайте в качестве значения свойства Text длинную текстовую строку (например, 12345678901234).

     • Label (label1); измените размеры элемента управления Label таким образом, чтобы он занимал большую часть области формы. В нем придется отображать текст, состоящий из нескольких строк.

     • Timer (timer1).

    4. Выделите компонент MainMenu в нижней части окна конструктора форм и добавьте следующие пункты меню:

     • Перейдите к крайнему слева меню (содержащему текст "Type Here" ("Набирайте здесь")) и введите Exit в качестве текста меню. Используя окно Properties, измените имя элемента меню с menuItem1 на menuItemExit. 

     • Справа от меню Exit, которое вы только что добавили (там, где находится текст "Type Here"), введите Prime Search в качестве текста меню. Примечание: при необходимости обратитесь к рис. 9.2.

     • Над меню Prime Search, которое вы только что добавили (там, где находится текст "Type Here"), введите Start в качестве текста меню. Используя окно Properties, измените имя элемента меню с menuItem2 на menuItemStart. Примечание: при необходимости обратитесь к рис. 9.2.

     • Ниже меню Start, которое вы только что добавили (там, где находится текст "Type Here"), введите Abort в качестве текста меню. Используя окно Properties, измените имя элемента меню с menuItem2 на menuItemStart. Примечание: при необходимости обратитесь к рис. 9.2. 

    5. Добавьте в проект новый класс. Назовите его FindNextPrimeNumber.сs. Замените содержимое кода класса в окне редактора кодом из листинга 9.5. 

    6. Перейдите обратно в окно Form1.cs [Design] и дважды щелкните на элементе меню Exit. В результате этого будет автоматически сгенерирована функция void menuItemExit_Click(), а фокус переместится в окно редактора кода. Введите для этой функции код из листинга 9.4. 

    7. Перейдите обратно в окно Form1.cs [Design] и дважды щелкните на элементе меню Start. В результате этого будет автоматически сгенерирована функция private void menuItemStart_Click(), а фокус переместится в окно редактора кода. Введите для этой функции код из листинга 9.4. 

    8. Перейдите обратно в окно Form1.cs [Design] и дважды щелкните на названии меню Abort. В результате этого будет автоматически сгенерирована функция private void menuItemAbort_Click(), а фокус переместится в окно редактора кода. Введите для этой функции код из листинга 9.4. 

    9. Перейдите обратно в окно Form1.CS [Design] и дважды щелкните на элементе управления timer1 в нижней части окна конструктора. В результате этого будет автоматически сгенерирована функция private void timer1_Tick(). Введите для этой функции код из листинга 9.4. 

    10. Введите в класс Form1.cs остальную часть кода из листинга 9.4. 

    11. Нажмите клавишу <F5> для компиляции кода и развертывания приложения в эмуляторе Smartphone.

    Запустив приложение, нажмите сначала кнопку телефона для вызова меню Prime Search, а затем — клавишу <1> для выбора пункта меню Start В результате этого начнется поиск простых чисел. В процессе выполнения поиска элемент управления timer1 будет несколько раз в секунду вырабатывать событие таймера, заставляющее пользовательский интерфейс обновлять отображаемый на форме текст с информацией о текущем состоянии. В результате динамического обновления этого текста, происходящего несколько раз в секунду, пользователь получает регулярное подтверждение того, что его запрос обрабатывается. Процесс поиска продолжается до тех пор, пока не будет найдено простое число или пока пользователь не выберет пункт Abort меню Prime Search и не нажмет кнопку <2>. Чтобы увеличить продолжительность поиска, перед тем, как его начать, введите в текстовом поле формы произвольное большое число. На моем эмуляторе поиск для начального числа 12345678901234 длился более 20 секунд. В том виде, как он есть, приведенный ниже код не препятствует запуску нового поиска в то время, когда предыдущий еще не закончился. Будет неплохо, если вы усовершенствуете приложение, введя в него проверку этого условия и прекращая выполнение текущего поиска, если к моменту запуска нового он еще не был закончен. Рекомендуется также, чтобы вы заглянули в код, представленный в листинге 9.5. и посмотрели, как ключевое слово lock используется для того, чтобы исключить параллельное вхождение различных потоков в критический раздел, не являющийся безопасным в отношении многопоточного выполнения.

    Листинг 9.4. Код, который должен быть помещен в класс Smartphone Form1.cs

    //------------------------------------------------------

    //Весь этот код должен находиться внутри класса Form1.cs

    //------------------------------------------------------


    //Объект, который будет выполнять все фоновые вычисления

    FindNextPrimeNumber m_findNextPrimeNumber;


    //--------------------------------------------

    //Обновить текст, информирующий о состоянии...

    //--------------------------------------------

    void setCalculationStatusText(string text) {

     label1.Text = text;

    }


    //-------------------------------------

    //Пункт меню для "выхода" из приложения

    //-------------------------------------

    private void menuItemExit_Click(object sender, System.EventArgs e) {

     this.Close();

    }


    //----------------------------------------

    //Пункт меню для начала фоновых вычислений

    //----------------------------------------

    private void menuItemStart_Click(object sender, System.EventArgs e) {

     //Число, с которого мы хотим начать поиск

     long startNumber = System.Convert.ToInt64(textBox1.Text);


     //Установить фоновое выполнение

     m_findNextPrimeNumber = new FindNextPrimeNumber(startNumber);


     //Запустить выполнение задачи в фоновом режиме

     m_findNextPrimeNumber.findNextHighestPrime_Async();

     //Установить таймер, используемый для контроля длительности вычислений

     timer1.Interval = 400;

     //400 мс timer1.Enabled = true;

    }


    //--------------------------------------------

    //Пункт меню для "отмены" выполняющейся задачи

    //--------------------------------------------

    private void menuItemAbort_Click(object sender, System.EventArgs e) {

     //He делать ничего, если вычисления не выполняются

     if (m_findNextPrimeNumber == null) return;

     //Установить поток в состояние прекращения выполнения

     m_findNextPrimeNumber.setProcessingState(FindNextPrimeNumber.ProcessingState.reguestAbort);

     //Немедленно известить пользователя

     //o готовности прекратить выполнение...

     setCalculationStatusText("Waiting to abort...");

     // setCalculationStatusText("Ожидание прекращения выполнения...");

    }


    //-------------------------------------------------------------

    //Этот таймер, вызываемый потоком пользовательского интерфейса,

    //позволяет отслеживать состояние выполнения

    //фоновых вычислений

    //-------------------------------------------------------------

    private void timer1_Tick(object sender, System.EventArgs e) {

     //Если к моменту вызова искомое простое число еще

     //не было найдено, отключить таймер

     if (m_findNextPrimeNumber == null) {

      timer1.Enabled =false;

      return;

     }

     //-------------------------------------------------

     //Если выполнение было отменено, освободить объект,

     //осуществляющий поиск, и выключить таймер

     //-------------------------------------------------

     if (m_findNextPrimeNumber.getProcessingState ==

         FindNextPrimeNumber.ProcessingState.aborted) {

      timer1.Enabled = false;

      m_findNextPrimeNumber = null;

     setCalculationStatusText("Prime search aborted");

      // setCalculationStatusText("поиск простого числа отменен");

      return;

     }

     //----------------------------------

     //Удалось ли найти правильный ответ?

     //----------------------------------

     if (m_findNextPrimeNumber.getProcessingState ==

         FindNextPrimeNumber.ProcessingState.foundPrime) {

      timer1.Enabled = false;

      //Отобразить результат

      setCalculationStatusText("Found! Next Prime = " + m_findNextPrimeNumber.getPrime().ToString());

      // setCalculationStatusText("Чиcлo найдено! Следующее простое число = " +

      // m_findNextPrimeNumber.getPrime().ToString());

      m_findNextPrimeNumber = null;

      return;

     }


     //--------------------------------------

     //Вычисления продолжаются. Информировать

     //пользователя о состоянии выполнения

     //--------------------------------------


     //Получить два выходных значения

     long numberCalculationsToFar;

     long currentItem;

     m_findNextPrimeNumber.getExecutionProgressInfo(out numberCalculationsToFar,out currentItem);

     setCalculationStatusText("In progress. Looking at: " +

      currentItem.ToString() + ". " +

      numberCalculationsToFar.ToString() +

      " calculations done for you so far!");

     // setCalculationStatusText("Вычисления продолжаются. Поиск в области: " +

     // currentItem.ToString() + ". " +

     // "Для вас выполнено " +

     // numberCalculationsToFar.ToString() +

     // " расчетов!");

    }

    Листинг 9.5. Код класса FindNextPrimeNumber.cs

    using System;

    public class FindNextPrimeNumber {

     //Перечисляем возможные состояния

     public enum ProcessingState {

      notYetStarted,

      waitingToStartAsync,

      lookingForPrime,

      foundPrime,

      requestAbort,

      aborted

     }


     long m_startPoint;

     long m_NextHighestPrime;


     //Поиск какого количества элементов выполнен?

     long m_comparisonsSoFar;


     //Для какого элемента сейчас выполняется поиск простого числа?

     long m_CurrentNumberBeingExamined;


     //Вызывается для обновления информации о состоянии выполнения

     public void getExecutionProgressInfo(out long numberCalculationsSoFar, out long currentItemBeingLookedAt) {

      //ПРИМЕЧАНИЕ. Мы используем блокирование потока для уверенности в том,

      //что эти значения не считываются во время выполнения операции

      //их записи. Поскольку доступ к m_comparisonsSoFar

      //и m_CurrentNumberBeingExamined могут осуществлять

      //одновременно несколько потоков, любая выполняемая над ними

      //операция записи/считывания должна синхронизироваться с "блокировкой",

      //что будет гарантировать "атомарность" этих операций

      lock (this) {

       numberCalculationsSoFar = m_comparisonsSoFar;

       currentItemBeingLookedAt = m_CurrentNumberBeingExamined;

      }

     }

     ProcessingState m_processingState;


     //---------------------------

     //Простейший конечный автомат

     //---------------------------

     public void setProcessingState(ProcessingState nextState) {

      //------------------------------------------------------

      //Простейший защитный код, гарантирующий

      //невозможность перехода в другое состояние, если задача

      //либо успешно завершена, либо успешно отменена

      //------------------------------------------------------

      if ((m_processingState == ProcessingState.aborted) ||

          (m_processingState == ProcessingState.foundPrime)) {

       return;

      }

      //Разрешить изменение состояния

      m_processingState = nextState;

     }


     public ProcessingState getProcessingState {

      get {return m_processingState;}

     }


     //------------------------

     //Возвращает простое число

     //------------------------

     public long getPrime() {

      if (m_processingState != ProcessingState.foundPrime) {

       throw new Exception("простое число еще не найдено!");

      }

      return m_NextHighestPrime;

     }


     //Конструктор класса

     public FindNextPrimeNumber(long startPoint) {

      setProcessingState(ProcessingState.notYetStarted);

      m_startPoint = startPoint;

     }


     //-----------------------------------------------------------

     //Создает новый рабочий поток, который будет вызывать функцию

     // "findNextHighestPrime()"

     //-----------------------------------------------------------

     public void findNextHighestPrime_Async() {

      System.Threading.ThreadStart threadStart;

      threadStart = new System.Threading.ThreadStart(findNextHighestPrime);

      System.Threading.Thread newThread;

      newThread = new System.Threading.Thread(threadStart);

      //Состояние должно отвечать, что поиск продолжается

      setProcessingState(ProcessingState.waitingToStartAsync);

      newThread.Start();

     }

     //-------------------------------------------------------------

     //Основной рабочий поток. Этот поток запускает поиск очередного

     //простого числа и выполняется до тех пор, пока не произойдет

     //одно из следующих двух событий:

     // (а) найдено очередное простое число

     // (b) от внешнего (по отношению к данному) потока поступила

     // команда прекратить выполнение

     //-------------------------------------------------------------

     public void findNextHighestPrime() {

      //Если поступила команда прекратить выполнение, то поиск

      //даже не должен начинаться

      if (m_processingState == ProcessingState.requestAbort) {

       goto finished_looking;

      }

      //Состояние должно отвечать, что поиск продолжается

      setProcessingState(ProcessingState.lookingForPrime);

      long currentItem;


      //Проверить, является ли число нечетным

      if ((m_startPoint & 1) == 1) {

       //Число является нечетным, начать поиск со следующего нечетного числа

       currentItem = m_startPoint + 2;

      } else {

       //Число является четным, начать поиск со следующего нечетного числа

       currentItem = m_startPoint + 1;

      }

      //Приступить к поиску простого числа

      while (m_processingState == ProcessingState.lookingForPrime) {

       //B случае нахождения простого числа возвратить его

       if (isItemPrime(currentItem) == true) {

        m_NextHighestPrime = currentItem; //Обновить состояние

        setProcessingState(ProcessingState.foundPrime);

       }

       currentItem = currentItem + 2;

      }

    finished_looking:

      //Выход. К этому моменту либо от другого потока поступила

      //команда прекратить поиск, либо было найдено и записано

      //следующее наибольшее простое число

      //Если поступил запрос прекратить выполнение,

      //сообщить, что выполнение процесса прекращено

      if (m_processingState == ProcessingState.requestAbort) {

       setProcessingState(ProcessingState.aborted);

      }

     }


     //Вспомогательная функция, которая проверяет, является

     //ли число простым

     private bool isItemPrime(long potentialPrime) {

      //Если число — четное, значит, оно не является простым

      if ((potentialPrime & 1) == 0) {

       return false;

      }

      //Продолжать поиск до тех пор, пока не будет превышено

      //значение квадратного корня из числа

      long end_point_of_search;

      end_point_of_search = (long)System.Math.Sqrt(potentialPrime) + 1;

      long current_test_.item = 3;

      while (current_test_item <= end_point_of search) {

       //---------------------------------------------------------

       //Проверить, не поступила ли команда прекратить выполнение!

       //---------------------------------------------------------

       if (m_processingState != ProcessingState.lookingForPrime) {

        return false;

       }

       //Если число делится без остатка,

       //значит, оно не является простым

       if (potentialPrime % current_test_item == 0) {

        return false;

       }

       //увеличить число на два

       current_test item = current_test_item + 2;

       //------------------------------------------

       //Увеличить количество проверенных элементов

       //------------------------------------------

       //ПРИМЕЧАНИЕ. Мы используем блокирование потока для уверенности в том,

       //что эти значения не считываются во время выполнения операции

       //их записи. Поскольку доступ к m_comparisonsSoFar

       //и m_CurrentNumberBeingExamined могут осуществлять

       //одновременно несколько потоков, любая выполняемая над ними

       //операция записи/считывания должна синхронизироваться с "блокировкой",

       //что будет гарантировать "атомарность" этих операций

       lock(this) {

        m_CurrentNumberBeingExamined = potentialPrime;

        m_comparisonsSoFar++;

       }

      }

      //Число является простым

      return true;

     } //Конец функции

    } //Конец класса

    Резюме 

    Фоновые потоки, если только их правильно использовать, могут пригодиться вам для улучшения условий интерактивного взаимодействия конечного пользователя с мобильным приложением за счет повышения способности пользовательского интерфейса к отклику. Фоновые потоки необходимо использовать умеренно, для вполне конкретных целей и лишь в тех случаях, когда они могут помочь вам справиться с реальными трудностями, связанными с обеспечением приемлемых интерактивных свойств приложения, которые не могут быть разрешены при использовании высокоприоритетного потока. Лучше всего проектировать приложение на основе однопоточной модели, привлекая фоновую обработку лишь тогда, когда вы убеждены в ее необходимости.

    При написании кода фоновых потоков и проектировании классов, к которым могут иметь доступ одновременно несколько потоков, следует проявлять особую осторожность. Весьма полезно тщательно документировать код приложения, явно указывая, к каким его фрагментам будут пытаться получить доступ одновременно несколько потоков, а к каким, что не менее важно, не будут. Постарайтесь, чтобы количество классов, функций, свойств и переменных-членов, которые должны совместно использоваться несколькими потоками, было как можно меньшим, и уделите повышенное внимание к проектированию, анализу, тестированию и документированию соответствующих фрагментов кода.

    Как и в случае других аспектов проектирования, при реализации асинхронной обработки вам могут очень пригодиться конечные автоматы. Конечные автоматы значительно расширяют возможности управления выполнением фоновых задач. Их использование делает возможным предоставление фоновыми потоками информации о состоянии выполнения, а также обращение других потоков с запросами к фоновому потоку на выполнение определенных действий, например, с запросом на прекращение выполнения фоновой работы. Плодотворным методом абстрагирования является инкапсуляция фоновой задачи в отдельном классе, что позволяет рассматривать ее как от дельную логическую единицу, для которой определены входные и выходные данные.

    Почти во всех случаях для управления пользовательским интерфейсом целесообразно предусматривать только один поток; этот поток должен быть основным потоком вашего приложения. Поток пользовательского интерфейса может осуществлять периодический опрос фоновых задач с целью получения информации о состоянии их выполнения и передавать эту информацию пользователю. В другом возможном варианте фоновые потоки могут передавать эту информацию пользовательскому интерфейсу посредством межпоточного взаимодействия, например, с помощью механизма Control.Invoke(), предоставляемого .NET Compact Framework.

    Потоки являются весьма полезным, но сложным инструментом. Как и в случае любой другой новаторской методики, существует риск чрезмерного использования многопоточного выполнения. Неоправданное применение нескольких потоков снижает общую производительность приложения, а излишняя сложность многопоточных моделей затрудняет сопровождение и отладку кода. Используйте многопоточность лишь в тех случаях, когда в этом действительно существует необходимость, привлекая для этого как можно более простые подходы. Следуя этим простым советам, вы сможете воспользоваться всеми преимуществами многопоточности и при этом избежите множества ловушек, связанных с использованием нескольких потоков выполнения кода.







     


    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх