Фоновые потоки, потребляющие 100% CPU на iPhone 3GS, вызывают скрытый основной поток

В моем приложении я выполняю 10 асинхронных NSURLConnections в NSOperationQueue как NSInvocationOperations. Чтобы предотвратить возврат каждой операции до того, как соединение сможет завершиться, я вызываю CFRunLoopRun(), как показано здесь:

- (void)connectInBackground:(NSURLRequest*)URLRequest {
 TTURLConnection* connection = [[TTURLConnection alloc] initWithRequest:URLRequest delegate:self];

 // Prevent the thread from exiting while the asynchronous connection completes the work.  Delegate methods will
 // continue the run loop when the connection is finished.
 CFRunLoopRun();

 [connection release];
}

Как только соединение заканчивается, окончательный селектор делегата соединения вызывает CFRunLoopStop(CFRunLoopGetCurrent()), чтобы возобновить выполнение в connectInBackground(), позволяя ему нормально вернуться:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    TTURLConnection* ttConnection = (TTURLConnection*)connection;
    ...
    // Resume execution where CFRunLoopRun() was called.
    CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {  
    TTURLConnection* ttConnection = (TTURLConnection*)connection;
    ...
    // Resume execution where CFRunLoopRun() was called.
 CFRunLoopStop(CFRunLoopGetCurrent());
}

Это работает хорошо, и это потокобезопасно, потому что я связал ответ и данные каждого соединения как переменные экземпляра в подклассе TTURLConnection.

NSOperationQueue утверждает, что оставляя его максимальное количество одновременных операций как NSOperationQueueDefaultMaxConcurrentOperationCount позволяет ему динамически регулировать количество операций, однако в этом случае он всегда решает, что 1 достаточно. Поскольку это не то, чего я хочу, я изменил максимальное число на 10, и теперь оно серьезно тянет.

Проблема заключается в том, что эти потоки (с помощью SpringBoard и DTMobileIS) потребляют все доступное время ЦП и приводят к тому, что основной поток становится латентным. Другими словами, как только процессор загружен на 100%, основной поток не обрабатывает события пользовательского интерфейса так быстро, как это необходимо для поддержания гладкого пользовательского интерфейса. В частности, прокрутка табличного представления становится нервной.

Process Name  % CPU
SpringBoard   45.1
MyApp         33.8
DTMobileIS    12.2
...

Пока пользователь взаимодействует с экраном или при прокрутке таблицы, приоритет основного потока становится равным 1,0 (максимально возможный), а режим цикла выполнения становится UIEventTrackingMode. Каждый из потоков операции имеет приоритет 0,5 по умолчанию, и асинхронные соединения выполняются в NSDefaultRunLoopMode. Из-за моего ограниченного понимания того, как потоки и их циклы выполнения взаимодействуют в зависимости от приоритетов и режимов, я в тупике.

Есть ли способ безопасно использовать все доступное время ЦП в фоновых потоках моего приложения, при этом гарантируя, что его основной поток отдает столько ЦП, сколько ему нужно? Возможно, заставляя основной поток запускаться так часто, как это необходимо? (Я думал, что приоритеты нити позаботятся об этом.)

ОБНОВЛЕНИЕ 12/23: Я наконец-то начал разбираться с процессором Sampler и нашел большинство причин, по которым пользовательский интерфейс стал нервным. Прежде всего, мое программное обеспечение вызывало библиотеку, в которой были семафоры взаимного исключения. Эти блокировки блокировали основной поток на короткие промежутки времени, вызывая незначительное пропускание прокрутки.

Кроме того, я нашел несколько дорогих вызовов NSFileManager и хэш-функций md5, которые выполнялись слишком долго. Слишком частое выделение больших объектов вызывало некоторые другие падения производительности в основном потоке.

Я начал решать эти проблемы, и производительность уже намного лучше, чем раньше. У меня 5 одновременных подключений, и прокрутка плавная, но у меня еще есть над чем поработать. Я планирую написать руководство по использованию CPU Sampler для выявления и устранения проблем, влияющих на производительность основного потока. Спасибо за комментарии, они были полезны!

ОБНОВЛЕНИЕ 14.01.2010: После достижения приемлемой производительности я начал понимать, что инфраструктура CFNetwork иногда теряла память. Исключения были случайным образом (но редко), возникающим и внутри CFNetwork! Я старался изо всех сил, чтобы избежать этих проблем, но ничего не получалось. Я совершенно уверен, что проблемы связаны с дефектами в самом NSURLConnection. Я написал тестовые программы, которые ничего не делали, кроме упражнений NSURLConnection, и они все еще давали сбой.

В итоге я заменил NSURLConnection на ASIHTTPRequest, и сбой полностью прекратился. CFNetwork почти никогда не протекает, однако при разрешении DNS-имени возникает еще одна очень редкая утечка. Я вполне доволен сейчас. Надеюсь, эта информация сэкономит вам время!

3 ответа

Решение

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

Оптимизируйте для отзывчивости пользователя, это единственное, что пользователь действительно замечает. Или (и мне очень неприятно это говорить) добавьте кнопку "Turbo" в ваше приложение, которое открывает неинтерактивный модальный диалог и увеличивает количество одновременных операций до 10, пока оно работает.

Звучит как будто NSOperationQueueDefaultMaxConcurrentOperationCount устанавливается на 1 по причине! Я думаю, вы просто перегружаете свой бедный телефон. Вы можете возиться с приоритетами потоков - я думаю, что ядро ​​Маха доступно и является частью официально благословенного API - но для меня это звучит как неправильный подход.

Одним из преимуществ использования "системных" констант является то, что Apple может настроить приложение для вас. Как вы собираетесь настроить это для запуска на оригинальном iPhone? Достаточно ли 10 для четырехлетнего iPhone следующего года?

Джеймс, хотя я не сталкивался с твоей проблемой, я успешно использовал синхронное соединение для загрузки в NSOperation подкласс.

NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:&requestError];

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

Другие вопросы по тегам