Простая и надежная поточная обработка с NSOperation
Потоки являются отличным способом решить много хитрых проблем. Например, если у Вас есть некоторое продолжительное вычисление, чтобы сделать, Вы действительно хотите отодвинуть ту работу к вторичному потоку, чтобы сохранить Ваш пользовательский интерфейс быстро реагирующим. Однако очень просто попасть в неприятное положение при использовании потоков в приложении для iOS. Потоки хитры к программе с в целом и, конечно, значительные части Касания Какао можно только вызвать от основного потока.
Этот technote описывает один способ управлять Вашим потоковым кодом, а именно, через NSOperation. Это - определенный пример общего метода, известного как ограничение потока, где Вы строго ограничиваете объем своего потокового кода, чтобы избежать традиционных проблем многопоточности, как доступ к структурам данных, в то время как они видоизменяются другим потоком, а также специфичными для платформы проблемами, как случайный вызов UIKit от вторичного потока.
Эта статья в частности написана для разработчиков приложения для iOS, но базовые методы, обсужденные здесь также, применяются к Mac OS X.
Введение
Потоки позволяют Вам решить две хитрых проблемы:
параллельное вычисление — В этом мире многожильного CPUs, много задач будут работать быстрее при делении работы между потоками, планирующимися на различные ядра.
Этот подход может быть полезным даже в одноядерной системе (как имеет место для всех текущих устройств на iOS). В частности, если Ваш поток вычисления тратит некоторую часть своего блокированного времени (например, ожидая диска I/O), можно использовать многократные потоки, чтобы сделать полезное вычисление на одном потоке, в то время как блокируется другой поток.
асинхронное вычисление — Если у Вас есть некоторое продолжительное вычисление, чтобы сделать и, как имеет место на iOS, необходимо сохранить основной поток быстро реагирующим, можно переместить то вычисление во вторичный поток. Это позволяет Вашему основному потоку обрабатывать вычисление, поскольку это было бы любая другая асинхронная работа.
Однако потоки являются не всегда правильным решением. Потоки имеют значительную стоимость, и с точки зрения памяти и с точки зрения, что еще более важно, кодируют сложность. Во многих случаях это - хорошая идея использовать асинхронный APIs, доступный в iOS вместо этого. В частности поток является плохим выбором, если Вы ожидаете, что он потратит существенное количество своего блокированного времени. Например:
периодическое выполнение — Запуск потока только для выполнения кода периодически никогда не является хорошей идеей. Необходимо использовать NSTimer вместо этого.
объединяясь в сеть — На типичном устройстве на iOS, CPU намного быстрее, чем сеть так при запуске потока, чтобы сделать сети синхронно, тот поток проведет большую часть своего блокированного времени. Лучший подход должен использовать асинхронный сетевой APIs, предоставленный iOS.
Как только Вы решили, что является надлежащим решить Вашу проблему с потоками, необходимо выбрать полную структуру кода. Общий подход должен использовать API NSThread непосредственно, или возможно использовать в своих интересах Какао -performSelectorInBackground:withObject: метод. Однако, очень просто столкнуться с проблемами с ними механизм. Первый раздел этого technote (Потоки И Их проблемы) описывает некоторые из тех проблем, именно так можно получить ощущение того, почему настолько важно быть тщательным с потоками. Следующий раздел (NSOperation К Спасению!) описывает, как можно использовать NSOperation для рассмотрения тех проблем простым и надежным способом. Наконец, последний раздел (Полезные советы NSOperations) предлагает некоторые полезные советы для использования NSOperation эффективно.
Потоки и их проблемы
Предположите, что у Вас есть приложение, выводящее на экран список чисел с общим количеством наверху. Фактически, Вы не должны воображать это, можно просто смотреть на Пример кода 'ListAdder', в частности создававшийся для этого technote. Рисунок 1 показывает основной UI.

Такое приложение могло бы иметь класс контроллера представления со свойством, которое является списком чисел, представленных как непостоянный массив. Перечисление 1 показывает, как это могло бы быть объявлено.
Перечисление 1 объявление массива чисел
@interface ListAdderViewController () [...] @property (nonatomic, retain, readonly ) NSMutableArray * numbers; [...] @end |
Теперь вообразите, только ради этого примера, то сложение списка чисел занимает очень долгое время. Так, поскольку пользователь изменяет список, необходимо обновить общее количество, но Вы не можете сделать этого на основном потоке, потому что это блокировало бы пользовательский интерфейс слишком долго. Таким образом необходимо сделать суммирование асинхронно. Один очевидный способ сделать, который должен запустить поток. Перечисление 2 показывает, как Вы могли бы сделать это. Вы отметите, что базовый цикл вычисления содержит длинную задержку так, чтобы можно было фактически видеть различные проблемы многопоточности, как они неожиданно возникают.
Перечисление 2 , Повторно вычисляющее на потоке
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ----
- (void)recalculateTotalUsingThread
{
self.recalculating = YES;
[self performSelectorInBackground:@selector(threadRecalculateNumbers) withObject:self.numbers];
}
- (void)threadRecalculateNumbers
{
NSAutoreleasePool * pool;
NSInteger total;
NSUInteger numberCount;
NSUInteger numberIndex;
NSString * totalStr;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
total = 0;
numberCount = [self.numbers count];
for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
NSNumber * numberObj;
// Sleep for a while. This makes it easiest to test various problematic cases.
[NSThread sleepForTimeInterval:1.0];
// Do the mathematics.
numberObj = [self.numbers objectAtIndex:numberIndex];
assert([numberObj isKindOfClass:[NSNumber class]]);
total += [numberObj integerValue];
}
// The user interface is adjusted by a KVO observer on recalculating.
totalStr = [NSString stringWithFormat:@"%ld", (long) total];
self.formattedTotal = totalStr;
self.recalculating = NO;
[pool drain];
}
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ---- |
Этот код имеет многочисленные проблемы. Первый довольно очевиден: конец -threadRecalculateNumbers применяет результаты к пользовательскому интерфейсу. При этом это заканчивает тем, что вызвало UIKit на вторичном потоке. Поэтому другие части программы используют наблюдение значения ключа (KVO) для наблюдения recalculating свойство и перезагрузка табличное представление соответственно. Если Вы изменяетесь recalculating на вторичном потоке этот код заканчивает тем, что вызвал UIKit на не позволяющемся вторичном потоке.
Решение той проблемы относительно просто. Перечисление 3 показывает замену для -threadRecalculateNumbers с фиксацией.
Перечисление 3 , Повторно вычисляющее на потоке, второй попытке
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ----
- (void)threadRecalculateNumbers
{
NSAutoreleasePool * pool;
NSInteger total;
NSUInteger numberCount;
NSUInteger numberIndex;
NSString * totalStr;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
total = 0;
numberCount = [self.numbers count];
for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
NSNumber * numberObj;
// Sleep for a while. This makes it easiest to test various problematic cases.
[NSThread sleepForTimeInterval:1.0];
// Do the mathematics.
numberObj = [self.numbers objectAtIndex:numberIndex];
assert([numberObj isKindOfClass:[NSNumber class]]);
total += [numberObj integerValue];
}
// Update the user interface on the main thread.
totalStr = [NSString stringWithFormat:@"%ld", (long) total];
[self performSelectorOnMainThread:@selector(threadRecalculateDone:) withObject:totalStr waitUntilDone:NO];
[pool drain];
}
- (void)threadRecalculateDone:(NSString *)result
{
// The user interface is adjusted by a KVO observer on recalculating.
self.formattedTotal = result;
self.recalculating = NO;
}
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ---- |
Так, мы сделаны, правильно? Неправильно! В то время как этот код решает упомянутую выше проблему, это все еще имеет другие проблемы. Одна такая проблема касается numbers свойство. Это - непостоянный массив, и код теперь получает доступ к массиву на одном потоке (вторичный поток), потенциально видоизменяя массив на другом (основной поток). Так, что происходит, если основной поток изменяется numbers выстройте, в то время как повторно вычисляет вторичный поток? Плохие вещи. Вообразите этот сценарий:
Пользователь добавляет элемент. Это запускает длинный перерасчет на потоке A.
Короткое время спустя перед потоком A завершается, пользователь тогда удаляет элемент. Это запускает другой длинный перерасчет на потоке B.
Теперь вспомните к основному циклу перерасчета. Для удобства это воспроизводится в Перечислении 4 со всем посторонним срезанным материалом.
Перечисление 4 разделенный вниз цикл перерасчета
NSUInteger numberCount;
NSUInteger numberIndex;
numberCount = [self.numbers count];
for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
NSNumber * numberObj;
numberObj = [self.numbers objectAtIndex:numberIndex];
total += [numberObj integerValue];
} |
Как Вы видите, код сначала получает количество числа элементов в массиве, затем выполняет итерации по этому многих элементов. Если пользователь удаляет элемент из numbers массив после вторичного потока инициализировал numberCount, этот код в конечном счете получит доступ к элементу от конца массива и катастрофического отказа.
Конечно, фиксация этого относительно проста: при запуске потока, Вы делаете неизменную копию numbers выстройте и имейте поток, воздействуют на это. Тем путем поток изолируется от изменений, сделанных основным потоком. Перечисление 5 показывает основную идею.
Перечисление 5 , Повторно вычисляющее на потоке, третьей попытке
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ----
- (void)recalculateTotalUsingThread
{
NSArray * immutableNumbers;
self.recalculating = YES;
immutableNumbers = [[self.numbers copy] autorelease];
assert(immutableNumbers != nil);
[self performSelectorInBackground:@selector(threadRecalculateNumbers:) withObject:immutableNumbers];
}
- (void)threadRecalculateNumbers:(NSArray *)immutableNumbers
{
NSAutoreleasePool * pool;
NSInteger total;
NSUInteger numberCount;
NSUInteger numberIndex;
NSString * totalStr;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
total = 0;
numberCount = [immutableNumbers count];
for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
NSNumber * numberObj;
// Sleep for a while. This makes it easiest to test various problematic cases.
[NSThread sleepForTimeInterval:1.0];
// Do the mathematics.
numberObj = [immutableNumbers objectAtIndex:numberIndex];
assert([numberObj isKindOfClass:[NSNumber class]]);
total += [numberObj integerValue];
}
totalStr = [NSString stringWithFormat:@"%ld", (long) total];
[self performSelectorOnMainThread:@selector(threadRecalculateDone:) withObject:totalStr waitUntilDone:NO];
[pool drain];
}
// ---- Broken Code ---- Do Not Use ---- Broken Code ---- Do Not Use ---- |
Это исправляет все отказывающие ошибки, связанные с потоковым перерасчетом. Однако в этом коде существует все еще много других серьезных ошибок. Самое очевидное связано с порядком, в который поступают результаты. Вообразите следующую последовательность:
Пользователь добавляет набор элементов к списку. Это начинает перерасчет, занимающий много времени (потому что время вычисления пропорционально длине массива). Это вычисление работает на потоке A.
Пользователь удаляет все кроме одного элемента из списка. Это начинает другой перерасчет, работая на потоке B, который завершается быстро.
Поток B завершает и применяет свои результаты к пользовательскому интерфейсу. Пользовательский интерфейс теперь показывает корректное число.
Поток A завершает и применяет свои результаты к пользовательскому интерфейсу. Это оставляет пользовательский интерфейс, показывающий устаревшие результаты.
Если тот тег все еще допустим, решение этой проблемы состоит в том, чтобы применить своего рода тег к работе перерасчета, и только фиксировать ее результаты. Это сам по себе не трудно сделать, но в этой точке необходимо начать понимать, что необходимо больше структурированного подхода. Одна хорошая структура для использования вот является NSOperation, обсужденным подробно в следующем разделе. Однако, прежде чем мы идем туда, необходимо рассмотреть две более нерешенных проблемы с потоковым кодом:
отмена — В вышеупомянутом сценарии Вы заканчиваете с потоком A и B, работающий одновременно с потоком результаты A, предназначенные, чтобы быть отброшенными. Было бы хорошо, если мы могли бы остановить поток так, чтобы это не тратило впустую циклы CPU, время работы от батареи и память.
ориентированное на многопотоковое исполнение освобождение — при выполнении какого-либо потокового кода в объектах UIKit (таких как контроллер представления) существует шанс что объект
-deallocметод вызовут на вторичном потоке. Это плохо (в смысле Охотников за привидениями того слова), особенно в чем-то как контроллер представления, где-deallocметод обычно выпускает объекты UIView, и следовательно мог бы вызвать их-deallocметод для работы вторичного потока. Можно узнать больше об этой проблеме и ее решениях в проблеме Освобождения.
NSOperation обеспечивает инфраструктуру для решения всех проблем, обсужденных выше. В следующем разделе мы посмотрим на то, как это работает.
NSOperation к спасению!
NSOperation является классом, позволяющим Вам моделировать асинхронные операции структурированным способом. Это обеспечивает путь к высокоуровневым частям Вашего приложения для запуска асинхронных операций, не волнуясь о подробных данных того, как они выполняются. NSOperation мог быть выполнен на потоке, или асинхронно через выполненные обратные вызовы цикла, или через другой, более экзотические средние значения. Важная вещь состоит в том, что не заботится высокоуровневый код Вашего приложения. Это только запускает работу и уведомляется, когда это закончено.
NSOperation мотивирует вызванное ограничение потока модели программирования. В этой модели ресурсы, используемые потоком, принадлежат тому потоку, не совместно использованному потоками. Это - отличный способ записать поточный код, не спотыкаясь за виды проблем, обсужденных в предыдущем разделе. Хороший способ использовать NSOperation:
Инициализируйте работу с неизменной копией данных, это должно выполнить свою работу.
Выполните работу; в то время как это выполняется, это только воздействует на данные, это было инициализировано с и на данных, которые это создает для себя и не совместно использует с другими потоками.
Когда работа завершается, она делает свои результаты доступными для остальной части приложения, в которой точке сама работа больше не касается тех результатов.
Используя отдельный NSOperation объект решает много проблем с общим потоковым кодом:
можно хранить вспомогательные данные, связанные с работой как свойства объекта NSOperation
можно использовать эти свойства, или даже сам объект NSOperation, как тег, чтобы решить, являются ли результаты работы устаревшими
объект NSOperation является дескриптором, которым можно отменить работу
можно гарантировать, что все потоковые выполнения кода в работе, и следовательно избегают проблем освобождения
NSOperation также имеет много других полезных функций:
Каждая работа выполняется очередью работы (NSOperationQueue). У каждой очереди есть максимальное количество операций, которые она выполнит параллельно (
maxConcurrentOperationCount), обычно известный как ширина очереди. Эти значения по умолчанию ширины к значению, это подходяще для устройства, на котором Вы работаете, но можно установить его в значение, это подходяще для задачи под рукой. Например, можно гарантировать, что операции сериализируются (выполняемый по одному) путем установки ширины очереди в 1. Или, если Ваши операции поражают сеть, можно использовать ширину очереди для ограничения числа параллельных сетевых операций.Можно создать очереди работы для моделирования пространства задач. Например, программа копирования файла могла использовать одну очередь работы на диск, чтобы избежать перегружать головку диска.
Можно сделать операции зависящими от других операций, и таким образом установить цепочки выполнения работы, и работа разветвляются на входе.
Важно понять, что NSOperation и NSOperationQueue не делают ничего, что Вы не могли сделать сами. Если бы Вы действительно хотели, то Вы могли бы тиражировать всю эту структуру с Вашим собственным кодом. Однако, если у Вас нет существующей структуры для управления асинхронным кодом, и Вы не испытываете желание повторно реализовывать колесо, NSOperation для Вас.
Остальная часть этого раздела описывает, как Пример кода 'ListAdder' использует NSOperation для реализации асинхронного перерасчета.
Используя NSOperation в ListAdder
Следующие несколько разделов описывают минимальную сумму работы, необходимой для принятия NSOperation в Примере кода 'ListAdder'.
Минимальный заголовок
Первый шаг в реализации асинхронного перерасчета с помощью NSOperation должен создать подкласс NSOperation, делающего вычисление. Необходимо обратить особое внимание на интерфейс работы, как выражено в его заголовочном файле. Самое главное все в том интерфейсе должно иметь четко определенную стратегию безопасности поток.
Перечисление 6 является абсолютным минимальным интерфейсом, требуемым AdderOperation от Примера кода 'ListAdder'.
Интерфейс операции Listing 6 Minimal
@interface AdderOperation : NSOperation - (id)initWithNumbers:(NSArray *)numbers; // only meaningful after the operation is finished @property (copy, readonly ) NSString * formattedTotal; @end |
Метод инициализации работы (в этом примере, -initWithNumbers:) должен взять, как параметры, вся информация, которую это абсолютно необходимо для работы для выполнения. В этом случае это просто берет список чисел для добавления.
Единственный другой элемент в этом минимальном интерфейсе является свойствами, которые клиент может использовать для достигания результата работы. В этом случае formattedTotal свойство является строкой, это предназначается, чтобы быть выведенным на экран в итоговом значении представления списка.
Работа наследовала различные важные свойства от NSOperation. Самое критическое isFinished свойство. Клиент может наблюдать, что это свойство (в смысле наблюдения значения ключа (KVO)) определяет, когда сделана работа. Также, если Вы не большой поклонник KVO, можно использовать другие механизмы для работы, чтобы сигнализировать, что это сделано. Например, Ваша работа могла бы отправить уведомление (использующий NSNotificationCenter), когда это сделано, или это могло бы определить протокол делегата.
Существует одна другая вещь отметить о минимальном заголовке, показанном в Перечислении 6: formattedTotal свойство является атомарным. Поэтому там вполне возможно, что к этому свойству получат доступ от многократных потоков одновременно, и свойство должно быть атомарным для этого для сейфа. Как правило все общедоступные свойства работы должны быть атомарными.
Минимальная реализация
Учитывая этот заголовок, работа должна включать реализацию для следующего:
-initWithNumbers:метод — Это немного тонко, и обсуждено подробно затем.formattedTotalсвойство — Это свойство может просто быть синтезировано.-deallocметод — нет никаких больших неожиданностей здесь; единственный глюк - то, что метод может быть выполнен любым потоком.-mainметод — Это - то, где Ваша работа делает свое вычисление и обсуждена подробно ниже.
Перечисление 7 показывает минимальную реализацию -initWithNumbers: метод.
Перечисление 7 минимальный метод инициализации
- (id)initWithNumbers:(NSArray *)numbers
{
assert(numbers != nil);
self = [super init];
if (self != nil) {
self->_numbers = [numbers copy];
assert(self->_numbers != nil);
}
return self;
} |
Здесь существует два момента, которых необходимо отметить:
Метод создает неизменную копию поступления
numbersмассив. Это предотвращает проблему, обсужденную ранее, где основной поток мог бы видоизменить массив, в то время как вторичный поток, выполняющий работу, работает над ним.Это - Ваше решение относительно того, сделать ли Ваш метод инициализации ориентированным на многопотоковое исполнение. Поскольку Вы управляете выделением работы, можно гарантировать, что это вызовут на основном потоке (или некотором другом определенном вторичном потоке, в этом отношении). Однако, если относительно просто сделать Ваш метод инициализации ориентированным на многопотоковое исполнение, необходимо сделать так.
Работа -main метод делает фактическое вычисление. Перечисление 8 показывает минимальную реализацию.
Перечисление 8 минимальный основной метод
- (void)main
{
NSUInteger numberCount;
NSUInteger numberIndex;
NSInteger total;
// This method is called by a thread that's set up for us by the NSOperationQueue.
assert( ! [NSThread isMainThread] );
// Do the heavy lifting (-:
total = 0;
numberCount = [self.numbers count];
for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
NSNumber * numberObj;
// Check for cancellation.
if ([self isCancelled]) {
break;
}
// Sleep for a second. This makes it easiest to test cancellation
// and so on.
[NSThread sleepForTimeInterval:1.0];
// Do the mathematics.
numberObj = [self.numbers objectAtIndex:numberIndex];
assert([numberObj isKindOfClass:[NSNumber class]]);
total += [numberObj integerValue];
}
// Set our output properties base on the value we calculated. Our client
// shouldn't look at these until -isFinished goes to YES (which happens when
// we return from this method).
self.formattedTotal = [self.formatter stringFromNumber:[NSNumber numberWithInteger:total]];
} |
Критические точки здесь включают:
Этот метод, как ожидают, будет работать на вторичном потоке. Все, чего это касается, должно иметь своего рода стратегию безопасности поток. В этом примере:
numbersсвойство ориентировано на многопотоковое исполнение, потому что оно может только быть изменено-initWithNumbers:метод, который, должно быть, обязательно завершился перед этим кодом, может работать.numberCount,numberIndex,totalиnumberObjпеременные безопасны, потому что они - локальные переменные, и таким образом не будут совместно использованы потоками.formattedTotalсвойство безопасно, потому что клиент не должен смотреть на него, пока не заканчивается работа. Даже если клиент проигнорирует это требование, то их доступ будет безопасен, потому что свойство является атомарным: клиент или получит исходное значение (т.е.nil) или правильное значение, никогда некоторая странная путаница.
Эксплуатационные испытания для отмены путем вызова
isCancelledв его основном цикле вычисления.
Поддержка отмены в параллельных операциях немного более хитра. При создании параллельной работы посмотрите Параллельные Операции для получения информации о том, как сделать это правильно.
Используя работу
Как только Вы переносите операцию, делающую задачу под рукой, необходимо использовать ее в приложении. Первый шаг создает NSOperationQueue, на котором можно выполнить работу. Когда контроллер представления инициализируется, в случае Примера кода 'ListAdder' основной контроллер представления объявляет частную собственность для очереди и затем инициализирует его. Перечисление 9 показывает подробные данные.
Перечисление 9 , Создающее очередь NSOperation
@interface ListAdderViewController () [...]
@property (nonatomic, retain, readonly ) NSOperationQueue * queue;
@property (nonatomic, retain, readwrite) AdderOperation * inProgressAdder;
[...]
@end
@implementation ListAdderViewController
[...]
- (id)init
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self != nil) {
[...]
self->_queue = [[NSOperationQueue alloc] init];
assert(self->_queue != nil);
[...]
}
return self;
}
[...]
@end |
Как только у Вас есть очередь работы, можно запустить операции путем простого добавления их к очереди. Перечисление 10 показывает этот процесс.
Перечисление 10 , Ставящее работу в очередь
- (void)recalculateTotalUsingOperation
{
// If we're already calculating, cancel that operation.
if (self.inProgressAdder != nil) {
[self.inProgressAdder cancel];
}
// Start up a replacement operation.
self.inProgressAdder = [[[AdderOperation alloc] initWithNumbers:self.numbers] autorelease];
assert(self.inProgressAdder != nil);
[self.inProgressAdder addObserver:self
forKeyPath:@"isFinished"
options:0
context:&self->_formattedTotal
];
[self.queue addOperation:self.inProgressAdder];
// The user interface is adjusted by a KVO observer on recalculating.
self.recalculating = YES;
} |
Существует несколько вещей отметить здесь:
Код отслеживает последний раз работу с очередями в
inProgressAdderсвойство. Когда это запускает новую работу, это заменяет то значение работой, которую это только что запустило. Это обладает двумя преимуществами:Когда это запускает работу, это может отменить предыдущий. Это означает, что новая работа только продолжает выполняться, используя ценные циклы CPU, время работы от батареи и память.
Когда работа завершается, она может определить, значимы ли результаты работы все еще. Больше на этом ниже.
Прежде, чем поставить работу в очередь, это добавляет наблюдение за
isFinishedсвойство. Когда работа завершена, это позволяет ему определить.
Наконец, когда работа завершена, необходимо проверить и фиксировать результаты. Перечисление 11 показывает, как Пример кода 'ListAdder' делает это.
Перечисление 11 Фиксируя результаты работы
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == &self->_formattedTotal) {
AdderOperation * op;
// If the operation has finished, call -adderOperationDone: on the main thread to deal
// with the results.
// can be running on any thread
assert([keyPath isEqual:@"isFinished"]);
op = (AdderOperation *) object;
assert([op isKindOfClass:[AdderOperation class]]);
assert([op isFinished]);
[self performSelectorOnMainThread:@selector(adderOperationDone:)
withObject:op
waitUntilDone:NO
];
} [...]
}
- (void)adderOperationDone:(AdderOperation *)op
{
assert([NSThread isMainThread]);
assert(self.recalculating);
// Always remove our observer, regardless of whether we care about
// the results of this operation.
[op removeObserver:self forKeyPath:@"isFinished"];
// Check to see whether these are the results we're looking for.
// If not, we just discard the results; later on we'll be notified
// of the latest add operation completing.
if (op == self.inProgressAdder) {
assert( ! [op isCancelled] );
// Commit the value to our model.
self.formattedTotal = op.formattedTotal;
// Clear out our record of the operation. The user interface is adjusted
// by a KVO observer on recalculating.
self.inProgressAdder = nil;
self.recalculating = NO;
}
} |
Существует две части к этому процессу. Во-первых, когда работа isFinished изменения свойства, KVO вызывает -observeValueForKeyPath:ofObject:change:context: метод. Этот метод работает на том же потоке как код, изменившийся isFinished свойство, в этом случае означающее, что это работает на вторичном потоке. Это не может управлять пользовательским интерфейсом от вторичного потока, таким образом, это задерживает работу к основному потоку при помощи -performSelectorOnMainThread:withObject:waitUntilDone: вызывать -adderOperationDone: метод.
-adderOperationDone: метод делает три вещи:
Это удаляет наблюдение за
isFinishedсвойство. Это балансирует код, добавивший наблюдение в Перечислении 10.Это проверяет, чтобы видеть, значимы ли результаты работы все еще путем сравнения завершенной работы (
op) против последний раз запущенной работы (inProgressAdder). Если они не соответствуют, результаты работы не имеют значения и отбрасываются.Если результаты работы все еще значимы, они посвящают себя пользовательскому интерфейсу.
Один неочевидный аспект этого целого процесса то, что inProgressAdder к свойству только когда-либо получает доступ основной поток. Это означает, что не должно быть атомарным, но, что еще более важно, этому не нужно никакое управление совместным выполнением. Кодовые последовательности как показанный в Перечислении 12 не были бы допустимы без этой неявной сериализации.
Перечисление 12 , Допустимое, не блокируя из-за основной сериализации потока
if (op == self.inProgressAdder) { [... do something with inProgressAdder ...] self.inProgressAdder = nil; } |
Это оборачивает базовый процесс для использования NSOperation для выполнения продолжительных вычислений асинхронно. Следующий раздел собирается пойти вне минимальной реализации, показанной ранее и покрыть некоторые более реалистические ситуации.
AdderOperation: вне основ
Перечисление 6 показало минимальный интерфейс AdderOperation. Это - сокращение вниз версия интерфейса, фактически используемого Примером кода 'ListAdder'. Полный интерфейс показан в Перечислении 13.
Интерфейс операции Listing 13 Actual
@interface AdderOperation : NSOperation
{
[... instance variables elided ...]
}
- (id)initWithNumbers:(NSArray *)numbers;
// set up by the init method that can't be changed
@property (copy, readonly ) NSArray * numbers;
@property (assign, readonly ) NSUInteger sequenceNumber;
// must be configured before the operation is started
@property (assign, readwrite) NSTimeInterval interNumberDelay;
// only meaningful after the operation is finished
@property (assign, readonly ) NSInteger total;
@property (copy, readonly ) NSString * formattedTotal;
@end |
Кроме того, этот интерфейс расширяется с помощью набора внутренних свойств, объявленных в расширении класса.
Расширение класса Работы перечисления 14
@interface AdderOperation () // only accessed by the operation thread @property (retain, readwrite) NSNumberFormatter * formatter; // read/write versions of public properties @property (assign, readwrite) NSInteger total; @property (copy, readwrite) NSString * formattedTotal; @end |
Вещь отметить вот состоит в том, что каждое свойство, объявленное в этих интерфейсах, имеет четко определенную стратегию безопасности поток. Стратегии включают:
неизменность — свойства Some устанавливаются во время инициализации и не изменяются после того.
numbersмассив является хорошим примером этого.время конфигурации только — свойства Some, как
interNumberDelay, может быть безопасно установлен между точкой, где работа создается и точка, где запускается работа. После той точки это или не безопасно или не эффективно изменить такие свойства.ограничение потока — свойства Some ориентированы на многопотоковое исполнение на основании того, чтобы быть полученным доступ только потоком, выполняющим работу.
formatterсвойство является примером этого метода.последовательное ограничение потока — свойства Some, как
totalиformattedTotal, первоначально только используются работой и затем, когда работа закончена, доступны другому коду.
Ограничение потока
Важно понять что AdderOperation formatter свойство является довольно искусственным примером ограничения потока. Это необходимо из-за очень ограниченного характера Примера кода сам 'ListAdder'. В реальном потоке приложения ограничение является намного более мощным методом. Например, Пример кода 'SeismicXML' начинает операцию для парсинга XML асинхронно. В этой работе существует объект NSXMLParser, и в том объекте объект синтаксического анализатора libxml2 XML. Те объекты можно вызвать от произвольного потока, но их можно только вызвать от одного потока за один раз. Ограничение потока позволяет безопасно использовать объекты как это от потокового кода, и NSOperation является отличным способом реализации ограничения потока.
Полезные советы NSOperations
Этот раздел содержит набор общих полезных советов для использования NSOperation.
NSOperation и GCD
Не сразу очевидно, как Grand Central Dispatch (GCD), представленная в iOS 4, касается NSOperation. Короткий ответ то, что эти две технологии дополнение друг друга приятно. GCD является низкоуровневый API, дающий Вам гибкость для структурирования кода во множестве различных путей. Напротив, NSOperation предоставляет Вам структуру по умолчанию, которую можно использовать для асинхронного кода. Если Вы ищете существующую, четко определенную структуру, это отлично адаптируется для приложений Какао, используйте NSOperation. Если Вы надеетесь создавать свою собственную структуру, точно соответствующую Ваше пространство задач, используйте GCD.
Проблема освобождения
Одна из самых больших проблем с использованием вторичных потоков от объекта UIKit, как контроллер представления, гарантирует, что Ваш объект освобожден безопасно. Этот раздел объясняет, как эта проблема возникает, и что можно делать с этим.
При запуске вторичного потока тому потоку свойственно сохранить целевой объект. Это происходит при многочисленных обстоятельствах, включая:
когда Вы запускаете вторичный поток с любого из следующих методов:
-performSelectorInBackground:withObject:-performSelector:onThread:withObject:waitUntilDone:-performSelector:onThread:withObject:waitUntilDone:modes:
когда Вы запускаете вторичный поток с NSThread
когда Вы выполняете блок асинхронно и блочные ссылки
selfили переменная экземпляра
Когда вторичный поток сохраняет целевой объект, необходимо гарантировать, что выпуски потока, что ссылка перед основным потоком выпускает свою последнюю ссылку на объект. Если Вы не делаете этого, последняя ссылка на объект выпущена вторичным потоком, что означает что объект -dealloc метод работает на том вторичном потоке. Это проблематично если объект -dealloc метод делает вещи, которые не безопасно сделать на вторичном потоке, что-то, что это характерно для объектов UIKit как контроллер представления.
Для конкретного примера этого рассмотрите код в Перечислении 15.
Перечисление 15 пример проблемы освобождения
- (IBAction)buttonAction:(id)sender
{
#pragma unused(sender)
[self performSelectorInBackground:@selector(recalculate) withObject:nil];
}
- (void)recalculate
{
while ( ! self.cancelled ) {
[... calculate ...]
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.cancelled = YES;
// race starts here
} |
В перечислении 15 -viewWillDisappear: наборы метода cancelled остановить вторичный поток, выполняющий его вычисление. Это запускает гонку между основным потоком, деловито разъединяющим контроллер представления и вторичный поток, замечающий cancelled устанавливаемое свойство и фактически выход. При большинстве обстоятельств вторичный поток выиграет гонки, выпустит его ссылку сначала, и все будет в порядке. Однако, если основной поток выиграет гонки и выпустит его ссылку перед вторичным потоком, то выпуск вторичного потока будет последним выпуском, и контроллер представления -dealloc метод будет работать на вторичном потоке.
Вы могли бы думать для обхождения этой проблемы путем опроса вторичного потока isFinished свойство, чтобы гарантировать, что это заканчивается прежде, чем возвратиться из -viewWillDisappear:. Однако вследствие неясных подробных данных реализации в NSThread, это, как гарантируют, не решит проблему.
Подобные проблемы могут возникнуть при использовании наблюдения значения ключа (KVO) для наблюдения isFinished свойство NSOperation. В то время как KVO не сохраняет или наблюдателя или наблюдание, это даже при удалении наблюдателя в Вашем все еще возможно -viewWillDisappear: метод, уведомление KVO могло бы уже быть в рейсе для Вашего объекта. Если это происходит, поток, выполняющий уведомление, мог бы закончить тем, что вызвал освобожденный объект!
Решение этой проблемы в общем случае довольно хитро. Однако при ограничении себя использованием NSOperation существует два относительно простых контура к решению:
сделайте все свое наблюдение значения ключа в постоянном объекте, один это никогда не освобождается
используйте класс QWatchedOperationQueue от Примера кода 'LinkedImageFetcher'
Параллельные операции
NSOperation поддерживает два типа операций:
стандартные операции — Эти операции, также известные как непараллельные операции, требуют, чтобы NSOperationQueue обеспечил параллелизм от их имени. NSOperationQueue организует для выполнения таких операций на потоке.
параллельные операции — Эти операции приносят свой собственный параллелизм. NSOperationQueue не должен выделять поток выполнению таких операций.
Когда базовые средства синхронны, стандартные операции являются отличным способом выполнить задачи асинхронно. Они обычно используются для продолжительных вычислений, но они могут также быть полезны для быстрого, надежного I/O (как диск I/O).
Напротив, параллельные операции являются большими, когда базовые средства являются асинхронными — нет никакого смысла связывающего поток для ожидания асинхронного API для окончания. Хорошим примером параллельной работы является тот, выполняющий Запрос HTTP с помощью API NSURLConnection.
Реализация параллельной работы правильно немного хитра. Необходимо смотреть на Пример кода 'LinkedImageFetcher' для примера того, как сделать это.
работа могла бы работать на потоке
работа могла бы работать асинхронно, любезность некоторого выполненного цикла базировала API
работа могла бы работать асинхронно, любезность некоторого GCD базировала API
работа могла бы работать в отдельном процессе
Кроме того, работа может измениться, как она работает, не требуя, чтобы изменился высокоуровневый код. Например, было бы очень просто изменить Пример кода 'ListAdder' для вызова веб-сервиса для выполнения дополнения. Только AdderOperation должен был бы измениться; высокоуровневый код был бы точно тем же.
Незаконченные операции
Некоторые плохо реализованные операции инициировали уведомление KVO о isFinished прежде чем они будут фактически закончены. При использовании KVO, чтобы определить, закончена ли работа, это - хорошая идея добраться isFinished свойство и подтверждает, что работа закончена, прежде чем Вы будете идти дальше, даже если это находится только в разовом отладкой утверждении. Для примера этого проверьте код в Перечислении 11.
История версии документа
| Дата | Примечания |
|---|---|
| 27.08.2010 | Новый документ, описывающий, как разработчики приложений Какао могут использовать NSOperation для решения многих проблем, свойственных от потокового кода. |