Использование iPhone мьютексов с асинхронными URL-запросами

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

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <NSCFArray: 0x3777c0> was mutated while being enumerated.'

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

Это результат выполнения асинхронных HTTP-запросов с помощью NSURLConnection, а затем использования NSNotification Center в качестве средства делегирования после завершения запросов. При запуске запросов, которые изменяют одни и те же наборы коллекций, мы получаем эти коллизии.


person Coocoo4Cocoa    schedule 16.02.2009    source источник


Ответы (6)


Если возможно, что какие-либо данные (включая классы) будут доступны из двух потоков одновременно, вы должны предпринять шаги для их синхронизации.

К счастью, Objective-C делает это до смешного просто с помощью ключевого слова synchronized. Это ключевое слово принимает в качестве аргумента любой объект Objective-C. Любые другие потоки, которые указывают тот же объект в синхронизированном разделе, будут остановлены до завершения первого.

-(void) doSomethingWith:(NSArray*)someArray
 {    
    // the synchronized keyword prevents two threads ever using the same variable
    @synchronized(someArray)
    {
       // modify array
    }
 }

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

// Get the semaphore.
id groupSemaphore = [Group semaphore];

@synchronized(groupSemaphore) 
{
    // Critical group code.
}
person Andrew Grant    schedule 16.02.2009

Есть несколько способов сделать это. Самым простым в вашем случае, вероятно, будет использование директивы @synchronized. Это позволит вам на лету создавать мьютекс, используя произвольный объект в качестве блокировки.

@synchronized(sStaticData) {
  // Do something with sStaticData
}

Другой способ - использовать класс NSLock. Создайте блокировку, которую хотите использовать, и тогда у вас будет немного больше гибкости, когда дело доходит до получения мьютекса (в отношении блокировки, если блокировка недоступна, и т. Д.).

NSLock *lock = [[NSLock alloc] init];
// ... later ...
[lock lock];
// Do something with shared data
[lock unlock];
// Much later
[lock release], lock = nil;

Если вы решите использовать любой из этих подходов, потребуется установить блокировку как для чтения, так и для записи, поскольку вы используете NSMutableArray / Set / что-то еще в качестве хранилища данных. Как вы видели, NSFastEnumeration запрещает изменение перечисляемого объекта.

Но я думаю, что другой проблемой здесь является выбор структур данных в многопоточной среде. Обязательно ли иметь доступ к вашим словарям / массивам из нескольких потоков? Или могли бы фоновые потоки объединить данные, которые они получают, а затем передать их основному потоку, который будет единственным потоком, которому разрешен доступ к данным?

person sbooth    schedule 16.02.2009
comment
Проблема в том, что «фоновые» потоки явно не созданы мной. Они являются результатом асинхронных запросов NSURLConnection. У меня нет возможности поговорить с основным потоком через код. Тем не менее, ваши другие предложения полезны, и я ценю это. - person Coocoo4Cocoa; 16.02.2009
comment
Я считаю, что делегат для NSURLConnection будет вызываться в потоке, который запустил операцию загрузки, не обязательно в потоке, создавшем объект. Таким образом, вы можете объединить данные в своих методах делегата. - person sbooth; 17.02.2009

В ответ на ответ sStaticData и NSLock (комментарии ограничены 600 символами), не нужно ли вам быть очень осторожным при создании объектов sStaticData и NSLock потокобезопасным способом (чтобы избежать очень маловероятного сценария множественных блокировок). созданы разными потоками)?

Я думаю, есть два обходных пути:

1) Вы можете поручить создание этих объектов в начале дня в единственном корневом потоке.

2) Определите статический объект, который автоматически создается в начале дня для использования в качестве блокировки, например статический NSString может быть создан встроенным:

static NSString *sMyLock1 = @"Lock1";

Тогда я думаю, вы можете смело использовать

@synchronized(sMyLock1) 
{
  // Stuff
}

В противном случае, я думаю, вы всегда будете попадать в ситуацию «курицы и яйца», создавая свои блокировки потокобезопасным способом?

Конечно, вы вряд ли столкнетесь с какой-либо из этих проблем, поскольку большинство приложений для iPhone работают в одном потоке.

Я не знаю о предложении [Групповой семафор] ранее, это тоже может быть решением.

person Dan J    schedule 25.08.2009
comment
«Предыдущий ответ»? Прямо сейчас есть два других ответа. - person Peter Hosey; 26.08.2009
comment
Теперь я обновил его, чтобы прояснить (FTR, я имел в виду ответ выше этого - учитывая, что я упоминаю sStaticData и NSLock, нет большой двусмысленности)! - person Dan J; 26.08.2009

N.B. Если вы используете синхронизацию, не забудьте добавить -fobjc-exceptions к вашим флагам GCC:

Objective-C обеспечивает поддержку синхронизации потоков и обработки исключений, которые описаны в этой статье и в разделе «Обработка исключений». Чтобы включить поддержку этих функций, используйте переключатель -fobjc-exceptions в GNU Compiler Collection (GCC) версии 3.3 и новее.

http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/ObjectiveC/Articles/ocThreading.html

person xmr    schedule 22.09.2010

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

NSMutableArray *originalArray = @[@"A", @"B", @"C"];
NSMutableArray *arrayToEnumerate = [originalArray copy];

Теперь измените arrayToEnumerate. Поскольку он не ссылается на originalArray, а является копией originalArray, это не вызовет проблемы.

person Gautam Jain    schedule 19.10.2015

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

 dispatch_async(serial_queue, ^{
    <#critical code#>
})

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

dispatch_sync(serial_queue Or concurrent, ^{
    <#critical code#>
})

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

person Kraming    schedule 09.06.2016