Книга: iOS. Приемы программирования

7.0. Введение

7.0. Введение

Параллелизм (конкурентное исполнение программ) возникает, когда одновременно выполняется несколько задач. Современные операционные системы позволяют параллельно выполнять задачи даже на одном процессоре. Такая возможность гарантируется, если выделить на каждую задачу строго определенный промежуток (квант) процессорного времени. Например, если за секунду требуется решить 10 задач и все они имеют одинаковый приоритет, то операционная система разделит 1000 мс на 10 и уделит решению каждой задачи 100 мс процессорного времени. Таким образом, все эти задачи решаются в течение одной секунды, и пользователю кажется, что это происходит параллельно.

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

Grand Central Dispatch (GCD) — это низкоуровневый API, написанный на языке С и работающий с блоковыми объектами. GCD отлично приспособлен для направления различных задач нескольким ядрам, так что программист может не задумываться о том, какое ядро решает какую задачу. Многоядерные устройства с операционной системой Mac OS X, в частности ноутбуки, имеются в свободном доступе уже довольно давно. А с появлением таких многоядерных устройств, как новый iPad, мы можем писать и интересные многопоточные приложения для системы iOS, рассчитанные на работу с несколькими ядрами.

Центральной составляющей GCD являются диспетчерские очереди. Диспетчерские очереди, как мы вскоре увидим, представляют собой пулы потоков, управляемые GCD в базовой операционной системе, будь то iOS или Mac OS X. Вы не будете работать с этими потоками напрямую. Вы будете иметь дело только с диспетчерскими очередями, распределяя задачи по этим очередям и приказывая очередям инициировать решение задач. GCD предлагает несколько режимов решения задач: синхронно, асинхронно, с определенной задержкой и т. д.

Чтобы приступить к использованию GCD в ваших приложениях, в проект не требуется импортировать каких-либо специальных библиотек. Apple уже встроила GCD в различные фреймворки, в частности в Core Foundation и Cocoa/Cocoa Touch. Все методы и типы данных, имеющиеся в GCD, начинаются с ключевого слова dispatch_. Например, dispatch_async позволяет направить задачу в очередь для асинхронного выполнения, а dispatch_after — выполнить блок кода после определенной задержки.

До того как появился GCD, программисту приходилось создавать собственные потоки для параллельного решения задач. Примерно такой поток разработчик iOS создаст для того, чтобы выполнить определенную операцию 1000 раз:

— (void) doCalculation{
/* Здесь происходят вычисления. */
}
— (void) calculationThreadEntry{
@autoreleasepool {
NSUInteger counter = 0;
while ([[NSThread currentThread] isCancelled] == NO){
[self doCalculation];
counter++;
if (counter >= 1000){
break;
}
}
}
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
/* Начинаем поток. */
[NSThread detachNewThreadSelector:@selector(calculationThreadEntry)
toTarget: self
withObject: nil];
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}

Программист должен создать поток вручную, а потом придать ему требуемую структуру (точку входа, автоматически высвобождаемый пул и основной цикл потока). Когда мы пишем аналогичный код с помощью GCD, нам на самом деле приходится сделать не так уж много. Мы просто помещаем наш код в блоковый объект и направляем этот блок в GCD для выполнения. Где именно будет выполняться данный код — в главном потоке или в каком-нибудь другом, — зависит именно от нас. Вот пример:

dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
size_t numberOfIterations = 1000;
dispatch_async(queue, ^(void) {
dispatch_apply(numberOfIterations, queue, ^(size_t iteration){
/* Здесь выполняется операция. */
});
});

В этой главе будет рассказано обо всем, что нужно знать о GCD. Здесь вы научитесь писать современные многопоточные приложения для iOS и Mac OS X, помогающие достичь впечатляющей производительности на таких многоядерных устройствах, как iPad 2.

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

 Главная очередь — занимается выполнением всех задач из главного потока, и именно здесь Cocoa и Cocoa Touch требуют от программиста вызывать все методы, относящиеся к пользовательскому интерфейсу. Пользуйтесь функцией dispatch_get_main_queue, помогающей управлять главной очередью.

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

 Последовательные очереди — всегда выполняют поставленные в них задачи по принципу «первым пришел — первым обслужен» (First In First Out, FIFO). При этом не имеет значения, являются эти задачи синхронными или асинхронными. Такой принцип работы означает, что последовательная очередь может выполнять в любой момент только один блок кода. Однако такие очереди не применяются в главном потоке, поэтому отлично подходят для решения задач, которые должны выполняться в строгом порядке и не блокировать при этом главный поток. Чтобы создать последовательную очередь, пользуйтесь функцией dispatch_queue_create.

Существуют два механизма отправки задач в диспетчерские очереди:

блочные объекты (см. раздел 7.1);

• функции C.

Блочные объекты позволяют наиболее эффективно использовать GCD и его огромный потенциал. Некоторые функции GCD были расширены таким образом, чтобы программист мог использовать функции C вместо блочных объектов. Однако в действительности лишь небольшое подмножество GCD-функций допускают замену объектов функциями C, поэтому перед дальнейшим изучением материала обязательно ознакомьтесь с разделом о блочных объектах (разделом 7.1).

Функции C, предоставляемые различным GCD-функциям, должны относиться к типу dispatch_function_t. Вот как этот тип определяется в библиотеках Apple:

typedef void (*dispatch_function_t)(void *);

Итак, если мы хотим, например, создать функцию под названием myGCDFunction, потребуется реализовать ее следующим образом:

void myGCDFunction(void * paramContext){
/* Вся работа выполняется здесь */
}

Параметр paramContext относится к контексту, который GCD позволяет передавать C-функциям при диспетчеризации задач к этим функциям. Вскоре мы подробно об этом поговорим.

Блочные объекты, передаваемые GCD-функциям, не всегда имеют одинаковую структуру. Некоторые должны принимать параметры, другие — нет, но ни один блочный объект, передаваемый GCD, не возвращает значения.

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

Блоковые объекты — это пакеты с кодом, которые в Objective-C обычно имеют форму методов. Блоковые объекты вместе с GCD образуют гармоничную среду, в которой можно создавать высокопроизводительные многопоточные приложения для iOS и Mac OS X. Вы можете спросить: «А что же такого особенного в блоковых объектах и GCD?» Ответ прост: больше никаких потоков! Все, что от вас требуется, — поместить код в блоковые объекты и перепоручить GCD выполнение этого кода.

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

Блоковые объекты в Objective-C — это сущности, которые в среде программистов принято называть объектами первого класса. Это означает, что вы можете создавать код динамически, передавать блоковый объект методу в качестве параметра и возвращать блоковый объект от метода. Все это позволяет более уверенно выбирать, что вы хотите делать во время исполнения и изменять ход действия программы. В частности, GCD может выполнять блоковые объекты в отдельных потоках. Поскольку блоковые объекты являются объектами Objective-C, с ними можно обращаться как с любыми другими объектами.

Иногда блоковые объекты называются замкнутыми выражениями (Closures).

Создание блоковых объектов напоминает создание обычных функций языка C, как будет показано в разделе 7.1. Блоковые объекты могут возвращать значения и принимать параметры. Блоковые объекты можно определять как встраиваемые либо обрабатывать как отдельные блоки кода наподобие функций языка C. При создании встраиваемым способом область видимости переменных, доступных блоковым объектам, существенно отличается от аналогичной области видимости, если блоковый объект реализуется как отдельный блок кода.

GCD работает с блоковыми объектами. При выполнении задач с помощью GCD вы можете передавать блоковый объект, код которого будет выполняться синхронно или асинхронно в зависимости от того, какие методы используются в GCD. Следовательно, вы можете создать блоковый объект, который будет отвечать за загрузку данных по URL (универсальному идентификатору ресурса), передаваемому в качестве параметра. Такой отдельный блоковый объект можно синхронно или асинхронно использовать в различных местах приложения на ваш выбор. Не требуется делать блоковый объект синхронным или асинхронным по сути. Вы всего лишь будете вызывать его с помощью тех или иных методов, относящихся к GCD, — синхронных или асинхронных, — и блоковый объект будет просто работать.

Блоковые объекты — довольно новое явление для программистов, создающих приложения для iOS и Mac OS X. На самом деле блоковые объекты пока еще уступают по популярности потокам, поскольку их синтаксис несколько отличается от организации обычных методов Objective-C и более сложен. Тем не менее потенциал блоковых объектов огромен, и Apple довольно активно внедряет их в свои библиотеки. Такие дополнения уже можно заметить в некоторых классах, например NSMutableArray. Здесь программист может сортировать массив с помощью блокового объекта.

Эта глава целиком посвящена созданию и использованию блоковых объектов в приложениях для iOS и Mac OS X, использованию GCD для передачи задач операционной системе, а также работе с потоками и таймерами. Я хотел бы подчеркнуть, что единственный способ освоить синтаксис блоковых объектов — написать несколько таких объектов самостоятельно. Изучите код примеров, которые сопровождают эту главу, и попробуйте реализовать собственные блоковые объекты.

В данной главе будут рассмотрены базовые вопросы, связанные с блоковыми объектами, а потом мы обсудим некоторые более сложные темы. В частности, поговорим об интерфейсе Grand Central Dispatch, о потоках, таймерах, операциях и очередях операций. Вы усвоите все, что необходимо знать о блоковых объектах, а потом перейдете к материалу о GCD. По моему опыту, лучше всего изучать блоковые объекты на примерах, поэтому данная глава изобилует примерами. Обязательно опробуйте их в Xcode, чтобы по-настоящему усвоить синтаксис блоковых объектов.

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

В Cocoa выполняются операции трех типов:

 блоковые операции — обеспечивают выполнение одного или нескольких блоковых объектов;

 активизирующие операции — позволяют активизировать метод в другом, уже существующем объекте;

 обычные операции — это классы обычных операций, от которых необходимо создавать подклассы. Код, который нужно выполнить, следует писать в методе main объекта операции.

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

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

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

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

• Операция может дождаться окончания выполнения другой операции и только после этого начаться. Будьте осторожны и не создавайте взаимозависимых операций. Такая распространенная ошибка называется взаимоблокировкой, или клинчем (Deadlock). Иными словами, нельзя ставить операцию А в зависимость от операции B, если операция B уже зависит от операции A. В таком случае они обе будут ждать вечно, расходуя память и, возможно, вызывая зависание приложения.

• Операции можно отменять. Так, если вы создаете подклассы от NSOperation, чтобы делать собственные виды объектов операций, обязательно пользуйтесь методом экземпляра isCancelled. Он применяется, чтобы проверить, не была ли отменена определенная операция, прежде чем переходить к выполнению задачи, связанной с этой операцией. Например, если задача вашей операции — проверять доступность соединения с Интернетом раз в 20 с, то перед каждым запуском операции нужно вызвать метод экземпляра isCancelled, чтобы сначала убедиться, что операция не отменена, и только после этого пытаться проверять наличие соединения с Интернетом. Если на выполнение операции уходит более нескольких секунд (например, если это загрузка файла), то при выполнении задачи нужно также периодически проверять метод isCancelled.

• Объекты операций обязаны выполнять «уведомление наблюдателей об изменениях в свойствах наблюдаемого объекта» (KVO, Key-Value Observing) на различных ключевых путях, в частности isFinished, isReady и isExecuting. В одной из следующих глав мы обсудим механизм KVO, а также KVC — механизм для доступа к полям объекта по именам этих полей.

• Если вы планируете создавать подкласс от NSOperation и выполнять специальную реализацию для операции, вам следует создать собственный автоматически высвобождаемый пул в методе main, относящемся к операции. Данный метод вызывается из метода start. Эти вопросы мы подробнее рассмотрим далее в этой главе.

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

Потоки и таймеры — это объекты, являющиеся подклассами от NSObject. Для порождения потока выполняется больше работы, чем для создания таймеров, а настройка цикла потока — более сложная задача, чем обычное слушание таймера, запускающего селектор. Когда приложение работает в операционной системе iOS, система создает для этого приложения как минимум один поток. Этот поток называется главным (Main Thread). Все потоки и таймеры должны добавляться в цикл исполнения (Run Loop). Цикл исполнения, как понятно из его названия, — это цикл, в ходе которого могут происходить разные события, например запуск таймера или выполнение потока. Обсуждение циклов исполнения выходит за рамки этой главы, но иногда я буду упоминать такой цикл.

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

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

Оглавление книги

Оглавление статьи/книги

Генерация: 0.060. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз