Шрифт:
Не допускайте того, чтобы поток пользовательского интерфейса блокировался на длительное время
По самой своей природе взаимодействие является синхронной операцией; ваше мобильное приложение должно передать некоторый блок данных на сервер или принять блок данных с сервера, причем для перемещения этого блока данных требуется некоторое время. Поскольку длительность прохождения данных до сервера, настольного компьютера или иного внешнего устройства ваше приложение непосредственно контролировать не может, важно не выполнять такого рода операции в том же потоке, в котором выполняется пользовательский интерфейс вашего приложения.
Разработчики приложений очень часто допускают ошибку, суть которой состоит в том, что для всех создаваемых коммуникационных систем сначала предусматривается их выполнение потоком пользовательского интерфейса в синхронном режиме, которое планируется заменить на более поздних стадиях разработки приложения выполнением коммуникационных задач фоновыми потоками. Как правило, такой подход не приводит к удовлетворительным результатам. Существует несколько причин, по которым разработчики попадают в эту ловушку:
■ Синхронные взаимодействия проще проектировать и отлаживать. Это истинная правда. Проектировать и отлаживать логику приложения, которая выполняется в синхронном режиме, значительно проще. По этой причине действительно рекомендуется, чтобы вы проектировали и отлаживали коммуникационную логику, выполняя ее синхронно с логикой пользовательского интерфейса вашего приложения. Коммуникационные процедуры, которые вы пишете, должны быть синхронными функциями. Однако, как только эти коммуникационные функции написаны, и их основная функциональность прошла тестирование, необходимо немедленно организовать их асинхронное по отношению к логике пользовательского интерфейса выполнение.
■ Синхронный код пишется гораздо быстрее. И это правда. Когда перед вами маячат сроки контрольного этапа, очень легко убедить себя в том, что единственный способ закончить работу вовремя — это срезать углы, организовав выполнение коммуникационной логики синхронно с выполнением логики пользовательского интерфейса.
■ Коль скоро мы не забываем о необходимости осуществления коммуникаций в асинхронном режиме и предусматриваем это в проекте приложения, то организовать впоследствии обмен данными в асинхронном режиме не составит никакого труда. Это — заблуждение. Несомненно, если вы не забываете о том, что, в конечном счете, коммуникационные операции должны будут выполняться асинхронно, то это облегчит вам переориентирование написанных вами функций синхронной связи на асинхронный режим выполнения в будущем, но одного этого еще мало. Истина состоит в том, что, как бы вы ни старались это предотвратить, в код, использующий синхронные процедуры, будут "намертво" встроены синхронные зависимости. Человек просто не в состоянии уследить за всеми неявными допущениями, которые вплетаются в логику приложения, и устранить возникающие из-за этого проблемы на более поздних этапах разработки приложения вам будет очень трудно.
Наилучшая методология проектирования коммуникационного кода, который должен обеспечивать постоянную способность пользовательского интерфейса к отклику, заключается в том, чтобы следовать приведенным ниже четырем рекомендациям:
1. Проектируйте свои коммуникационные процедуры в виде отдельных, надежно инкапсулированных функций. Лучший способ подготовить коммуникационные процедуры к асинхронному выполнению — это надежно их инкапсулировать. Коммуникационная процедура, которая пересылает данные на сервер, должна передавать статическую копию этих данных; для получения этих данных функции не должен требоваться доступ к каким-либо глобальным либо разделяемым состояниям. Аналогичным образом, коммуникационная процедура, осуществляющая считывание данных с сервера, не должна изменять никакое глобальное состояние приложения, пока не прочитает все данные. Очень важно соблюдать эти принципы, поскольку одновременная работа двух потоков с одним и тем же глобальным состоянием приложения — это верный путь к дополнительным сложностям, разрушению данных и снижению надежности приложения. Поток пользовательского интерфейса должен иметь исключительный доступ к данным, с которыми он работает, коммуникационным процедурам должна предоставляться копия этих данных, а взаимодействие между обеими системами должно осуществляться лишь в строго определенных и надежно протестированных точках.
2. Тестируйте коммуникационные процедуры, вызывая их в синхронном режиме. Как уже отмечалось ранее, тестировать и отлаживать коммуникационный код намного проще, если он выполняется в синхронном режиме потоком пользовательского интерфейса. По этой причине гораздо целесообразнее сначала выявить все ошибки в коде, выполняющемся синхронно с пользовательским интерфейсом, и лишь после этого переходить к тестированию кода при его выполнении фоновым потоком. Я рекомендую вам помещать на форму большую кнопку с надписью "Тестировать и сохранить код" и использовать ее для тестирования и отладки своих коммуникационных процедур. Причина, по которой я рекомендую использовать именно "большую кнопку", заключается в том, что такая кнопка выглядит уродливо и бросается в глаза, так что вы никогда не забудете удалить ее.
3. Ужесточайте условия тестирования своих коммуникационных процедур, вызывая их в асинхронном режиме в намеренно затрудненных ситуациях, используя тестовое приложение. Код, выполняемый фоновым потоком, всегда оказывается сложнее синхронно вызываемого кода. Кроме того, отслеживать все тонкие разновидности повреждения данных и логические ошибки в управлении состоянием, возникающие в условиях, когда несколько различных потоков взаимодействуют между собой самым непредсказуемым образом, весьма нелегко, что дополнительно осложняется наличием многофункционального кода приложения, который окружает отлаживаемый код. Лучший способ создания действительно надежных систем — это жесткое тестирование кода в упрощенном окружении, предназначенном для помещения кода в специально осложненные состояния, и мониторинг выполнения кода для выявления необычных ситуаций. Целесообразно создать специальное приложение, в котором несколько потоков кода выполняются асинхронно, и тщательно исследовать внутреннее состояние приложения для выявления любых возможных неожиданных результатов. Тестирование такого рода может указать вам на необходимость введения некоторых ограничений в выполняемый код, которые не позволят приложению переходить в состояния повышенного риска; так, код, предназначенный для выполнения фоновой задачи, может активно препятствовать выполнению нескольких экземпляров этой задачи параллельными потоками, если вы обнаружили, что надежность такого выполнения является проблематичной, а использование многопоточности не является обязательным. Тестированный, усовершенствованный и отлаженный подобным образом код можно включать в код приложения с гораздо большей уверенностью, нежели код, который считается заведомо корректным исключительно на основании голого оптимизма.
4. Проведя тестирование кода, немедленно преобразуйте коммуникационные процедуры, чтобы они могли выполняться в рабочем фоновом потоке вашего приложения. После отладки базового коммуникационного кода в синхронном режиме и его тестирования в асинхронном режиме он будет готов к помещению в асинхронную операцию, выполняемую в вашем приложении. Приложение должно основываться на понятной и согласованной модели выполнения кода рабочим фоновым потоком, и эту же модель следует применять для всех ваших асинхронных коммуникационных потребностей. Эту модель асинхронного выполнения всегда необходимо использовать и при встраивании коммуникационного кода в пользовательский интерфейс приложения. Не встраивайте синхронные вызовы коммуникационных процедур в пользовательский интерфейс, планируя впоследствии, когда у вас появится время, преобразовать их для выполнения в асинхронном режиме; чем больше кода вы сюда поместите, тем больше зависимостей будет создано.