Использование синхронизированного массива Singleton с NSThread

У меня есть приложение для книг с UISearchBar, где пользователь вводит любое название книги и получает результаты поиска (из вызова ext API) ниже по мере ввода.

Я использую одноэлементную переменную в своем приложении с именем retrievedArray, в которой хранятся все книги.

@interface Shared : NSObject {
    NSMutableArray *books;
}

@property (nonatomic, retain) NSMutableArray *books;

+ (id)sharedManager;

@end

Доступ к этому осуществляется в нескольких файлах .m с помощью NSMutableArray *retrievedArray; ...в заголовочном файле

retrievedArray = [[Shared sharedManager] books];

Мой вопрос заключается в том, как я могу гарантировать, что значения внутри retrievedArray остаются синхронизированными во всех классах.

На самом деле значения внутри retrievedArray добавляются через NSXMLParser (т.е. через API внешней веб-службы). Есть отдельный файл XMLParser.m, где я делаю весь парсинг и заполняю массив. Парсинг выполняется в отдельном потоке.

    - (void) run: (id) param  {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL: [self URL]];
        [parser setDelegate: self];
    [parser parse];
        [parser release];

        NSString *tmpURLStr = [[self URL]absoluteString];

        NSRange range_srch_book = [tmpURLStr rangeOfString:@"v1/books"];

        if (range_srch_book.location != NSNotFound) 
            [delegate performSelectorOnMainThread:@selector(parseDidComplete_srch_book) withObject:nil waitUntilDone:YES];

        [pool release];
    } 


    - (void) parseXMLFile: (NSURL *) url
    {   
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        [self setURL: url];
        NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                     selector:@selector(run:)


object: nil];
    [retrievedArray removeAllObjects];
    [myThread start];
    [pool release];
}

Кажется, что есть некоторые проблемы с синхронизацией, если пользователь печатает очень быстро (похоже, он работает нормально, если пользователь печатает медленно).... Таким образом, есть 2 представления, в которых отображается содержимое объекта в этом элементе общего массива; Список и детали. Если пользователь быстро печатает и нажимает A в представлении списка, ему показывается B в подробном представлении... Это основная проблема.

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

РЕДАКТИРОВАНИЕ ДЛЯ ПРИМЕРА ПРОБЛЕМЫ СИНХРОНИЗАЦИИ: В представлении списка, если отображаются 3 элемента, скажем, Элемент1, Элемент2 и Элемент3, и если пользователь щелкает Элемент2, ему отображается Элемент3 в подробном представлении (т. е. неправильные детали)

Ниже приведен код, который выполняется при щелчке элемента в представлении списка;

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Navigation logic -- create and push a new view controller

    if(bookdetailCustom == nil)
        bookdetailCustom = [[BookDetailCustom alloc] initWithNibName:@"BookDetailCustom" bundle:[NSBundle mainBundle]];

    //aBook = [retrievedArray objectAtIndex:indexPath.row];

    bookdetailCustom.selectedIndex = indexPath.row;

    [self.navigationController pushViewController:bookdetailCustom animated:YES];
    [bookdetailCustom release];
    bookdetailCustom = nil;
}

Вот как выглядит searchTabkleView

- (void) searchTableView {
    NSString *searchText = searchBar.text;
    NSMutableArray *searchArray = [[NSMutableArray alloc] init];

    for (int i=0;i<[retrievedArray count];i++)
    {
        Stock *aBookTemp = [retrievedArray objectAtIndex:i];
        NSString *temp = [aBookTemp valueForKey:@"BookName"];
        [searchArray addObject:temp];
    }

    for (NSString *sTemp in searchArray)
    {
        NSRange titleResultsRange = [sTemp rangeOfString:searchText options:NSCaseInsensitiveSearch];

        if (titleResultsRange.length > 0)
            [copyListOfItems addObject:sTemp];
    }

    [searchArray release];
    searchArray = nil;
}

Пожалуйста, предложите некоторые подходящие исправления.


person copenndthagen    schedule 27.02.2011    source источник


Ответы (5)


Из того, что вы опубликовали, каждый извлекаемый массив указывает на один и тот же объект NSMutableArray. Таким образом, нет никаких отдельных массивов для синхронизации, это один и тот же массив.

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

Я предполагаю, что последовательность событий примерно такая:

  1. В представлении «Список» отображается набор результатов, включающий A с индексом N.
  2. Пользователь что-то печатает. Синтаксический анализатор XML начинает постепенно обновлять общий массив. Представление списка еще не обновлено.
  3. Пользователь касается элемента с индексом N в представлении списка. Представление списка указывает представлению сведений отображать элемент с индексом N.
  4. Представление «Подробности» извлекает элемент с индексом N из общего массива, но из-за обновления, начатого на шаге 2, индекс N теперь содержит B. Что и отображается в представлении «Подробности».
  5. В какой-то момент синтаксический анализ XML завершается, и теперь список обновляется.

Также должно быть возможно, если загрузка и синтаксический анализ из веб-службы достаточно медленные, чтобы шаг 4 просто завершился сбоем с NSRangeException.

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

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

person Anomie    schedule 27.02.2011
comment
Эй, Аномия... Мне было очень трудно объяснить здесь проблему. Но вы, кажется, поняли проблему ОЧЕНЬ ТОЧНО... Теперь, переходя к решению, я хотел бы перейти ко второму предложенному вами подходу. Другим может быть то, что синтаксический анализатор накапливает результаты в частном массиве и сразу обновляет общий массив непосредственно перед сигналом представления списка для обновления самого себя. Возможно ли, чтобы вы предоставили псевдокод того, что вы пытаетесь сказать? . Я могу реализовать то же самое в своем приложении и посмотреть, работает ли оно. - person copenndthagen; 28.02.2011
comment
Но да, как я уже сказал, поскольку проблема возникает только тогда, когда пользователь печатает очень быстро, похоже, это как-то связано со временем, затрачиваемым на обновление массива в 2 местах. Еще раз спасибо за вашу помощь в этом. Я приложил все усилия, чтобы решить проблему, но безуспешно, и теперь я действительно отчаянно пытаюсь решить эту проблему. - person copenndthagen; 28.02.2011
comment
В ваших методах NSXMLParserDelegate вы должны добавлять объекты в retrievedArray. Вместо этого добавьте поле temporaryArray рядом с полем retrievedArray, задайте для него новый NSMutableArray непосредственно перед вызовом [parser parse], добавьте результаты во временный массив в методах делегата, а затем после того, как [parser parse] вернет вызов [retrievedArray replaceObjectsInRange:NSMakeRange(0,retrievedArray.count) withObjectsFromArray:temporaryArray]. - person Anomie; 28.02.2011
comment
Или, если вы немного поработаете, чтобы сигнализировать всем о перезагрузке из [[Shared sharedManager] books] (или просто обратиться к нему напрямую, вместо того, чтобы везде копировать указатель на извлекаемый массив), вы можете просто использовать [[Shared sharedManager] setBooks:temporaryArray]. - person Anomie; 28.02.2011
comment
Привет, Аноми. Спасибо за вашу помощь. Я пытаюсь понять предложенный вами подход и реализовать его в своем приложении. Я немного не понимаю второй подход (сигнализируйте всем, чтобы перезагрузить извлекаемый массив). Не могли бы вы немного рассказать об этом... Я также верну свои выводы для 1-го подхода, как только смогу. Еще раз спасибо. - person copenndthagen; 01.03.2011
comment
Привет, Аноми... Я только что попытался реализовать предложенный вами 1-й подход. Я только отредактировал класс XMLParser (т. е. заменил в методах делегата извлекаемый массив на временный массив. Также после завершения синтаксического анализа я использовал [retrievedArray replaceObjectsInRange: NSMakeRange (0, retrievedArray.count) withObjectsFromArray: TemporaryArray]; значения заполняются во временном массиве ..Однако проблемы с синхронизацией все еще возникают между списком и подробным представлением.То есть, если я нажму на элемент 1, подробности отобразятся для элемента 3. Пожалуйста, предложите. - person copenndthagen; 01.03.2011
comment
Разрабатывая второй подход: если вы используете [[Shared sharedManager] setBooks:temporaryArray], это полностью заменит массив, который вы получите от [[Shared sharedManager] books]. Но все, что уже сделало это и сохранило результат в retrievedArray, не заметит изменения, им придется сделать retrievedArray = [[Shared sharedManager] books]; еще раз, чтобы обновить его. Или вы можете полностью избавиться от retrievedArray как ivar и вместо этого назначить локальную переменную непосредственно из [[Shared sharedManager] books] в начале каждого метода, которому нужен список книг. - person Anomie; 01.03.2011
comment
Хм... Возможно, вам следует отредактировать вопрос, чтобы включить обработчик нажатия на Item1. - person Anomie; 01.03.2011
comment
хорошо.. Я отредактировал вопрос, включив в него пример Item1,2,3... Я также попытаюсь понять второй подход и посмотреть, работает ли он в моем приложении. Я свяжусь с вами, как только у меня будет результаты. - person copenndthagen; 02.03.2011
comment
Я имел в виду, что вы включаете фактический код, который выполняется при нажатии на один из элементов. - person Anomie; 03.03.2011
comment
хорошо .. Я отредактировал свой вопрос, включив в него код выбора. Но так и не смог решить проблему... - person copenndthagen; 04.03.2011
comment
Пожалуйста, дайте мне знать, если мне нужно предоставить дополнительную информацию. - person copenndthagen; 04.03.2011
comment
Anomie: У вас была возможность просмотреть обновленный код, который я добавил? - person copenndthagen; 07.03.2011
comment
У меня заканчиваются идеи. Как теперь выглядит синтаксический анализатор XML после всех вышеперечисленных изменений, что делает parseDidComplete_srch в контроллере представления списка, как выглядит tableView:cellForRowAtIndexPath: и как выглядит любой код в BookDetailCustom, который фактически загружает данные? - person Anomie; 07.03.2011
comment
Я отредактировал свой вопрос, включив в него код searchTableView... parseDidComplete_srch использует searchTableView cellForRowAtIndexPath, использует copyListOfTems для отображения объекта indexpath.row. В BookDetailCustom он использует индекс извлеченного массива для отображения в подробном представлении. Эта информация передается ему из представления списка. - person copenndthagen; 07.03.2011
comment
Считаете ли вы, что почти дублированный массив копий copyListOfItems может вызвать проблему. ? - person copenndthagen; 07.03.2011
comment
Кажется, теперь я вижу вашу проблему. Строки в представлении «Список» соответствуют индексам в copyListOfItems, а в представлении «Подробности» — в retrievedArray. Эти массивы не имеют одинакового содержимого! Вы можете сделать что-то вроде bookdetailCustom.selectedIndex = [retrievedArray indexOfObject:[copyListOfItems objectForIndex:indexPath.row]]; или просто изменить BookDetailCustom, чтобы он брал объект вместо пути индекса. - person Anomie; 07.03.2011
comment
Я только что попробовал bookdetailCustom.selectedIndex = [retrievedArray indexOfObject:[copyListOfItems objectForIndex:indexPath.row]]; Но он выдает ошибку для objectForIndex: (не распознает/за пределами границ). Также он показывает повторяющиеся элементы в представлении списка, если я очень быстро печатаю в строке поиска. Не уверен, в чем причина... В любом случае, я почти уверен, что проблема связана с этими двумя массивами copylistofitems и retrievedArray... Вы хотите, чтобы я открыл новый вопрос, где я специально выделю 2 массива? ? - person copenndthagen; 08.03.2011
comment
Ба, это все равно бы не сработало, я упустил из виду тот факт, что retiredArray содержит объекты, а copyListOfItems содержит только строку с названием книги. Как я уже говорил ранее, лучше всего, вероятно, сделать так, чтобы каждая ячейка таблицы содержала фактический объект Stock, а BookDetailCustom брала объект Stock вместо индекса. - person Anomie; 08.03.2011
comment
хорошо.. Я обновил код в представлении списка до stockdetailCustom.aBook = [retrievedArray objectAtIndex:indexPath.row]; и в деталях просмотрите NSString *tempOffName = aBook.OfficialName; Он подробно показывает то же имя, что и выбранный элемент LIST. Но через некоторое время я получаю следующую ошибку: [NSMutableArray objectAtIndex:]: индекс 20 за пределами [0 .. 19]' Не уверен, связано ли это с этим... Но просто добавлю, что в моем представлении списка также отображаются повторяющиеся элементы... должно отображаться только около 10 элементов. . - person copenndthagen; 08.03.2011
comment
Подводя итог, я хочу, чтобы произошли следующие вещи; 1. Показать только около 10 уникальных элементов в представлении списка 2. Показать щелкнутый элемент в подробном представлении 3. Не показывать дубликаты (API не возвращает дубликатов... это потому, что массив не очищается... хотя я использовал removeAllObjects) 4. Это должно работать, даже если пользователь печатает очень быстро. - person copenndthagen; 08.03.2011
comment
Когда вы меняете список, вы вызываете reloadData в табличном представлении? Если вы этого не сделаете, возможно, это может вызвать как дубликаты, так и возможное исключение. В противном случае в меню «Выполнить» в XCode есть «Остановка при исключениях цели-C»; включите это, и он остановится, когда возникнет это исключение, а затем вы можете использовать обычные элементы управления отладчика, чтобы точно узнать, где в вашей программе происходит доступ за пределы. - person Anomie; 08.03.2011
comment
Да, я вызываю reloadData для своей таблицы. Итак, на данный момент статус 1) Список не перезагружается быстро, если пользователь быстро вводит в строке поиска. 20 Тем не менее, синхронизация списка/деталей является правильной, т. е. если щелкнуть ABC в представлении списка, он покажет детали ABC в подробном представлении. (даже если пользователь мог быстро набрать ABD, который не обновлялся быстро/правильно) В любом случае, я пока приму ответ и открою новый вопрос, описывающий вышеуказанный статус. - person copenndthagen; 13.03.2011
comment
Спасибо за все ваши усилия на этом .. Через некоторое время я открою новый. - person copenndthagen; 13.03.2011

Первоначально я предложил вам удалить ключевое слово nonatomic из объявления вашего свойства. По умолчанию используется Atomic (параметр atomic отсутствует, достаточно опустить nonatomic), который будет обеспечивать безопасность потоков, заключая синтезированный сеттер в блок @synchronize.

К сожалению, многие люди научились просто ставить nonatomic по всему коду, не понимая его по-настоящему. Я всегда думал, что это происходит из-за копирования / вставки из примера кода Apple — они часто используют его для вещей, связанных с пользовательским интерфейсом — помните, что UIKit не является потокобезопасным.

Аноми указал в своем ответе, что это не так, скорее всего, потому что вы мутируете изменяемый массив из разных потоков. Это звучит как правильный ответ для меня - я бы удалил свой ответ, но оставлю его здесь, так как я думаю, что мои комментарии чего-то стоят (но не на 100% относятся к вашей проблеме).

person makdad    schedule 27.02.2011
comment
Спасибо за ответ... Я удалил неатомарные свойства из файла Shared.h @property (retain) NSMutableArray *books; Но проблема все же возникает... - person copenndthagen; 27.02.2011
comment
Наличие атомарного геттера и сеттера означает, что получение и установка массива будут атомарными, но доступ к содержимому массива по-прежнему не будет. Вы правы, что он должен быть атомарным, но ему также нужно использовать @synchronize вокруг кода, который изменяет/читает массив. Было бы проще и эффективнее использовать решение @Anomie по использованию отдельного массива во время обновления, и в этом случае будет достаточно атомарных сеттеров. - person ughoavgfhw; 08.03.2011
comment
Если бы объект Shared имел методы @synchronized для управления массивом книг, одновременный доступ к содержимому массива был бы решен. - person Ricardo de Cillo; 11.03.2011

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

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

Это стоит попробовать, и я надеюсь, что это поможет.

person Nick Toumpelis    schedule 10.03.2011

Попробуйте использовать NSRecursiveLock в средствах доступа к массиву.

См. NSRecursiveLock документация. Из обзора:

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

Пример кода CoreVideo содержит примеры правильного использования.

person Mark    schedule 02.03.2011
comment
Я никогда раньше не слышал и не использовал NSRecursiveLock. Не могли бы вы подробнее рассказать о том, как реализовать то же самое в моем приложении. - person copenndthagen; 04.03.2011
comment
Почему рекурсивная блокировка? Разве обычного замка недостаточно? - person ughoavgfhw; 08.03.2011

Проблема в том, что на retrievedArray ссылаются два потока. Удалите все ссылки на retrievedArray из кода синтаксического анализа XML и измените его только в основном потоке.

Вот процесс:

  1. Измените parseXMLFile:, чтобы создать новый массив: parsedArray = [NSMutableArray array]
  2. Измените parser:didEndElement:, чтобы добавить к этому новому массиву: [parsedArray addObject:aBook]
  3. В parser:didEndDocument: передайте ваш новый массив в основной поток:

    [delegate performSelectorOnMainThread: @selector(updateRetrievedArray:)
                               withObject: parsedArray
                            waitUntilDone: NO];
    
  4. updateRetrievedArray:, работающий в основном потоке, будет кодом, ответственным за обновление retrievedArray — таким образом, только один поток изменяет этот объект:

    - (void) updateRetrievedArray: (NSArray *)parsedArray {
        [retrievedArray setArray:parsedArray];
        [self parseDidComplete_srch_book]; // Be sure to call [tableView reloadData]
    }
    
person skue    schedule 05.03.2011
comment
retrievedArray — это общий массив, который обновляется в XMLParser следующим образом. Сначала все объекты удаляются каждый раз, когда выполняется синтаксический анализ; - (void) parseXMLFile: (NSURL *) url { [retrievedArray removeAllObjects]; .... } Затем в parser:didEndElement( f([elementName isEqualToString:@BookDetails]) [retrievedArray addObject:aBook]; } сообщите мне, если вам понадобятся какие-либо дополнительные сведения. - person copenndthagen; 05.03.2011
comment
Я обновил свой ответ на основе вашего комментария. Попробуйте и дайте мне знать, если это не решит вашу проблему. - person skue; 07.03.2011
comment
Да, конечно. Я попробую реализовать то же самое в своем приложении. Я вернусь с выводами. - person copenndthagen; 07.03.2011
comment
Всего 1 быстрый вопрос... Для шага 3 ...parser:didEndDocument: вы имеете в виду parser:didEndElement: Пожалуйста, подтвердите - person copenndthagen; 07.03.2011
comment
Я попробовал код выше. Но по некоторым причинам он не работает нормально. Я не могу правильно разобрать. - person copenndthagen; 07.03.2011
comment
Нет, № 3 должен быть, когда вы закончите синтаксический анализ документа, если вы обновляли таблицу. Каждый раз, когда вы анализируете элемент, вы добавляете его в свой parsedArray. Когда вы закончите синтаксический анализ документа, вы отправляете parsedArray в основной поток для обновления извлекаемого массива. Но если ваш код не анализируется правильно, то это вообще отдельная проблема... - person skue; 08.03.2011