Здравствуйте!
— Не хотите ли войти
Оглавление

Ждущие потоки и зачем они могут понадобиться на практике

28 июля 2010
Уровень сложности: для разработчиков с небольшим опытом

Аннотация

Речь пойдет о решении не типовой задачи программирования на языке Objective-C с использованием Cocoa Touch. Будут затронуты некоторые общие моменты многопоточного программирования, разобран вопрос создания потока, подобного главному потоку приложения. Всё это происходит на примере исправления довольно критического бага класса NSURLConnection. В конце статьи приведён весь исходный код, который вы можете свободно использовать в своих проектах.

Вступление

Зачем это нужно

Проблема заключается в том, что на платформе iPhone OS 3.x, в классе NSURLConnection нельзя проставить таймаут соединения. (подробнее можно почитать, например, тут) Есть пути решения с асинхронным вызовом и отменой выполнения запроса с помощью таймера, который эмулирует таймаут, но что делать, если необходим именно синхронный доступ? Одним из возможных решений является создание обёртки вокруг асинхронного вызова и реализации таким образом аналога метода sendSynchronousRequest:returningResponse:error: класса NSURLConnection. Этим и займёмся, а попутно научимся делать кое-какие интересные вещи.

Архитектура решения

То, как будет работать наш аналог, схематично изображено на рисунке:

Архитектура

Давайте посмотрим, что здесь происходит. Пусть в процессе выполнения нашего основного потока, решающего какую-нибудь задачу, возникает необходимость получить синхронно какие-нибудь данные с удалённого сервера. Буквой «А» обозначено время, когда основной поток вызывает наш аналог метода sendSynchronousRequest. После этого происходит следующее:

  1. Блокируется основной поток исполнения (мы ведь реализуем синхронный метод).
  2. Создается новый поток («рабочий поток»).
  3. Внутри рабочего потока создается активный объект класса NSURLConnection. Вместе с ним создается таймер, установленный на необходимый интервал ожидания.
  4. Созданные объекты активируются: начинают загружаться данные и тикать время, отведенное на процесс.
  5. Если срабатывает таймер, а запрос еще не закончен, то нужно прервать процесс закачки данных. С другой стороны, если заканчивается запрос, то нужно остановить таймер.
  6. В точке «B» информация об успешности загрузки и её результаты передается основному потоку, после чего он разблокируется и продолжает свою работу.

Всё было бы хорошо и очень просто, если бы не одно «НО».

«Ждущие» потоки

Подводный камень

Когда я впервые посмотрел на этот план, мне всё показалось лёгким и понятным. Но на самом деле тут есть некоторая проблема с рабочим потоком. Дело в том, что мы возлагаем на него нестандартные обязанности: он должен сидеть и ждать сообщений от объекта класса NSURLConnection. Но как так сделать? И можно ли?
На самом деле, можно. Аналогичной функциональностью обладает главный поток приложения, который ожидает событий интерфейса, чтобы их обработать и опять вернуться в состояние «висения». Однако создать что-нибудь подобное самому оказывается не такой простой задачей. И прежде чем продолжить, стоит сказать несколько слов про «внутренности» потоков; это поможет нам с вами лучше понимать друг друга в дальнейшем.

Вскрытие потоков: что внутри?

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

Попробуем представить себе, как выглядит главный поток приложения (на рисунке он изображен ввиде зубастого Pacman’а). У него есть очередь, состоящая из блоков команд, которые он по одному достаёт и исполняет. Если очередь пустеет, поток блокируется и ждёт пополнения. Новый блок кода может добавиться по сотне причин: может быть, какое-либо сообщение пришлёт операционная система, а может пользователь ткнет в кнопку, и тогда в очередь добавится обработчик этого события.

Обычные потоки, которые создаются парой строчек кода, выглядят в точности также, но с одним «но»: они выполняют только один блок кода, по завершении которого сразу же завершаются. И этот блок — тело самого потока, которое мы с вами пишем при создании.

Таким образом такой поток просто не пригоден для использования в качестве целевого для асинхронных вызовов методов (которые как раз и занимаются тем, что добавляют в очередь потока блоки кода). И такой расклад нас с вами, естественно, не устраивает, потому что асинхронная работа NSURLConnection построена по шаблону активного объекта, который в свою очередь основывается на асинхронных вызовах методов. А значит все обработчики событий, вызванные объектом NSURLConnection (вспоминаем нашу архитектуру), просто не будут выполнены. А должны бы.

Cocoa Framework спешит на помощь

Итак, для реализации нашей задумки нужно написать поток, способный ждать и выполнять блоки кода, которые ему откуда-либо посылаются. Задача выглядит низкоуровневой и, увы, достаточно скудно освещена в сети (или мало индексирована Гуглом?..).

Но оказывается, всё не так грустно, как кажется с первого взгляда. По крайней мере, до этой сферы дотянулся своими лапами замечательный Cocoa Framework, к помощи которого мы и прибегнем.

Единственным классом, который нам понадобится, будет NSRunLoop. Мы сделаем следующее: напишем поток с таким телом, которое будет блокировать себя, ожидая блоки кода в очереди, а потом исполнять их, как только они появляются. Звучит сложно? Без паники. Это гораздо проще, чем кажется. Давайте просто посмотрим на код, а потом уже будем понимать, что в нём творится.

Итак, три значимые строчки тела потока перед вами:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port]
         forMode:NSDefaultRunLoopMode];
while ([runLoop runMode:NSDefaultRunLoopMode
             beforeDate:[NSDate distantFuture]]);

Что же тут происходит? Давайте разбираться!

Вдумчивый взгляд

Начнём с начала.

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

Тут не сложно: мы просто получаем объект, с помощью которого будем совершать доступ к очереди потока.
Для того, чтобы понять вторую строчку, давайте сначала разберём цикл.

while ([runLoop runMode:NSDefaultRunLoopMode
             beforeDate:[NSDate distantFuture]]);

Вот это уже интереснее. Словами русского языка этот цикл можно описать так: «пока происходит что-то непонятное, проверяй ещё и ещё, что это непонятное продолжает происходить». На деле вызывается всего один метод runMode:beforeDate:. Метод является блокирующим и ждёт до определённого времени (время указано вторым его аргументом) пришествия в очередь потока какого-нибудь кода. После этого он этот код исполняет и возвращает true.

Однако метод иногда возвращает false. Это может происходить по двум причинам:

  1. Произошло что-то специфическое, из-за чего «цикл выполнения не может быть запущен» (дословный перевод из документации)
  2. Когда runLoop не находит потенциальных источников событий.

Если первый случай от нас на прямую не зависит, то обеспечение потока необходимыми источниками — целиком наша обязанность. С нею с успехом справляется вторая строчка кода

[runLoop addPort:[NSMachPort port]
         forMode:NSDefaultRunLoopMode];

Теперь, я надеюсь, вам уже почти всё понятно. Запутанными должны оставаться только «режимы» (те самые параметры forMode: и.т.д.), на которые я до настоящего момента везде закрывал глаза. Исполнение в том или ином режиме — это выполнение типов блоков кода в очереди, специфичных для этого режима. Остальные блоки игнорируются.

Хоть сколько-нибудь подробное описание «режимов» и всего того, что к ним прилагается, не входит в цель этой статьи. Это достаточно большая тема, достойная отдельного изложения. Подробнее про это и многое другое можно прочитать здесь.

Итоги

Промежуточный результат и его использование

На основании всего вышесказанного вы теперь можете сами легко реализовать «ждущие» потоки. Однако я вполне допускаю, что вы устали вчитываться и понимать, или у Вас нет времени, или, в конце концов, Вам просто не нравятся мои рисунки. В таком случае, в самом конце, вы можете найти мой код, применяющий знания, изложенные в этой статье, на практике. Пользоваться этим классом очень легко:

KGIdleThreadManager* backgroundThread = 
	[[KGIdleThreadManager alloc] init];

Теперь в вашем распоряжении есть «ждущий» поток, на который можно посылать сообщения для выполнения:


[self performSelector:@selector(foo)
             onThread:backgroundThread.thread
           withObject:nil
        waitUntilDone:false];

Важно помнить, что объект не уничтожится, если вы не вызовете метод stop, который остановит «ждущий» поток.

[backgroundThread stop];
[backgroundThread release];

Возвращение к истокам

Напомню, с чего всё начиналось: мы собирались реализовать свой аналог для метода sendSynchronousRequest. Для этого мы разработали архитектуру, решили большую техническую проблему, стоящую на пути. Всё что остаётся — взять клавиатуру и воплотить идею в жизнь. Мою реализацию вы можете найти в конце статьи. Самое сложное в ней — синхронизация двух потоков с помощью класса NSCondition, поэтому я не буду останавливаться на её коде. Рассмотрим только использование.

Класс предоставляет всего два метода. Рассмотрим их по очереди:

- (NSData *)send:(NSURLRequest *)request
         timeout:(NSTimeInterval)_timeout 
           error:(NSError **)error;

Этот метод посылает созданный запрос и возвращает байты, присланные сервером в ответ. Если произошло что-либо непредвиденное, метод возвращает nil.

- (void)stop;

Этот метод прерывает процесс скачивания. Так как сам процесс синхронный, то его можно и нужно вызывать из другого потока.

Заключение

Хотелось бы сказать, что описание функционирования потоков как PacMan’ов с очередями из блоков кода является очень грубым и изложено здесь для наглядности.

Термин «ждущего» потока введён автором и вряд ли является хоть сколько распространенным.

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

Спасибо за внимание!

Файлы статьи

Комментарии к документу
Зарегистрируйтесь или войдите, чтобы оставить комментарий.

[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
Может лучше было бы конечную дату поставить? Чтобы можно было поток сбросить в любое время.

NULL AUTHOR

Можно, но идеально, чтобы цикл прокручивался как можно реже и только по необходимости. Поэтому в практической реализации используется флаг и хитренькая реализация метода stop. Можете посмотреть сами в исходнике KGIdleThreadManager

Так ведь он не сможет проверить состояние переменной running до того как выйдет из runLoop, значит «backgroundThread» будет ждать событий пока программа не будет прибита системой.

NULL AUTHOR

Я бы решил задачу как-то так: gist.github.com/514947

NULL AUTHOR

«Так ведь он не сможет проверить состояние переменно running..» Действительно, не сможет. Именно поэтому функция stop делает две вещи: ставит running в false и искусственно генерирует событие, чтобы цикл прокрутился. Таким образом остановка произойдёт фактически мгновенно.
Ваше решение совершает лишние действия, однако неплохо на практике.

Действительно, я как-то забыл о том факте, что «[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]» возвращает управления после получения _одного_ события.

NULL AUTHOR
© 2009-2012, ООО «Инру»
Вход
Имя пользователя:
Пароль:
Или…
Twi
Отмена
Войти
Восстановить забытый пароль…