Лучший способ заставить NSRunLoop ждать установки флага? - PullRequest
39 голосов
/ 29 сентября 2008

В документации Apple для NSRunLoop приведен пример кода, демонстрирующий приостановку выполнения при ожидании установки флага чем-то другим.

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

Я использовал это, и оно работает, но при исследовании проблемы с производительностью я отследил его до этого куска кода. Я использую почти точно такой же кусок кода (просто имя флага другое :), и если я добавлю NSLog в строку после установки флага (в другом методе), а затем строку после while() между двумя инструкциями журнала, которые кажутся случайными, происходит несколько случайное ожидание.

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

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

NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (webViewIsLoading && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil])
  loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];

Используя этот код, операторы журнала при установке флага и после цикла while теперь постоянно находятся на расстоянии менее 0,1 секунды.

У кого-нибудь есть идеи, почему оригинальный код демонстрирует такое поведение?

Ответы [ 7 ]

36 голосов
/ 29 сентября 2008

Runloops может быть чем-то вроде волшебной коробки, где вещи просто случаются.

По сути, вы говорите циклу выполнения обработать некоторые события и затем вернуться. ИЛИ вернитесь, если он не обрабатывает события до истечения времени ожидания.

При тайм-ауте в 0,1 секунды вы чаще его используете. Runloop срабатывает, не обрабатывает никаких событий и возвращает за 0,1 секунды. Иногда у него будет шанс обработать событие.

С вашим тайм-аутом distantFuture runloop всегда будет ждать, пока не обработает событие. Поэтому, когда он возвращается к вам, он только что обработал какое-то событие.

Значение короткого тайм-аута будет потреблять значительно больше ЦП, чем бесконечное, но есть веские причины для использования короткого тайм-аута, например, если вы хотите завершить процесс / поток, в котором запускается runloop. Возможно, вы захотите runloop, чтобы заметить, что флаг изменился и что он должен выручить как можно скорее.

Возможно, вы захотите поиграть с наблюдателями runloop, чтобы вы могли точно увидеть, что делает runloop.

См. этот документ Apple для получения дополнительной информации.

15 голосов
/ 07 октября 2008

Хорошо, я объяснил вам проблему, вот возможное решение:

@implementation MyWindowController

volatile BOOL pageStillLoading;

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

    // Simmulate web page loading
    sleep(5);

    // This will not wake up the runloop on main thread!
    pageStillLoading = NO;

    // Wake up the main thread from the runloop
    [self performSelectorOnMainThread:@selector(wakeUpMainThreadRunloop:) withObject:nil waitUntilDone:NO];

    [pool release];
}


- (void) wakeUpMainThreadRunloop:(id)arg
{
    // This method is executed on main thread!
    // It doesn't need to do anything actually, just having it run will
    // make sure the main thread stops running the runloop
}


- (IBAction)start:(id)sender
{
    pageStillLoading = YES;
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil];
    [progress setHidden:NO];
    while (pageStillLoading) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    [progress setHidden:YES];
}

@end

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

Это только один способ, как вы можете это сделать. Уведомление, размещаемое и обрабатываемое в главном потоке, может быть предпочтительным (также для него могут регистрироваться другие потоки), но решение, представленное выше, является самым простым, о котором я могу подумать. Кстати, это не очень потокобезопасно. Чтобы действительно быть потокобезопасным, каждый доступ к логическому значению должен быть заблокирован объектом NSLock из любого потока (использование такой блокировки также делает «volatile» устаревшим, поскольку переменные, защищенные блокировкой, являются неявными volatile согласно стандарту POSIX; Однако стандарт C не знает о блокировках, поэтому здесь только volatile может гарантировать работу этого кода; GCC не нужно устанавливать volatile для переменной, защищенной блокировками).

11 голосов
/ 26 октября 2008

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

Если вы хотите запустить модально - например, с отображением панели прогресса - запустите модально! Продолжайте и используйте методы NSApplication, запустите модально для таблицы прогресса, затем остановите модальное, когда загрузка будет завершена. См. Документацию Apple, например, http://developer.apple.com/documentation/Cocoa/Conceptual/WinPanel/Concepts/UsingModalWindows.html.

Если вы просто хотите, чтобы представление работало в течение всей загрузки, но вы не хотите, чтобы оно было модальным (например, вы хотите, чтобы другие представления могли реагировать на события), тогда вам следует что-то сделать намного проще Например, вы можете сделать это:

- (IBAction)start:(id)sender
{
    pageStillLoading = YES;
    [NSThread detachNewThreadSelector:@selector(runInBackground:) toTarget:self withObject:nil];
    [progress setHidden:NO];
}

- (void)wakeUpMainThreadRunloop:(id)arg
{
    [progress setHidden:YES];
}

И все готово. Не нужно контролировать цикл выполнения!

-Wil

10 голосов
/ 30 сентября 2008

Если вы хотите иметь возможность установить переменную своего флага и немедленно уведомить цикл выполнения, просто используйте -[NSRunLoop performSelector:target:argument:order:modes:, чтобы попросить цикл запуска вызвать метод, который устанавливает флаг в значение false. Это приведет к немедленному вращению цикла выполнения, к методу, который будет вызван, и к тому же будет проверен флаг.

5 голосов
/ 06 октября 2008

В вашем коде текущий поток будет проверять, чтобы переменная изменялась каждые 0,1 секунды. В примере кода Apple изменение переменной не будет иметь никакого эффекта. Runloop будет работать, пока не обработает какое-либо событие. Если значение webViewIsLoading изменилось, событие не генерируется автоматически, поэтому оно останется в цикле, почему оно выйдет из него? Он останется там до тех пор, пока не получит какое-то другое событие, а затем вырвется из него. Это может произойти через 1, 3, 5, 10 или даже 20 секунд. И пока это не произойдет, он не выйдет из цикла запуска и, следовательно, не заметит, что эта переменная изменилась. IOW код Apple, который вы цитировали, является неопределенным. Этот пример будет работать только в том случае, если изменение значения webViewIsLoading также создает событие, которое вызывает пробуждение runloop, и, похоже, это не так (или, по крайней мере, не всегда).

Я думаю, вы должны переосмыслить проблему. Поскольку ваша переменная называется webViewIsLoading, вы ждете загрузки веб-страницы? Вы используете Webkit для этого? Я сомневаюсь, что вам вообще нужна такая переменная или какой-либо код, который вы опубликовали. Вместо этого вы должны кодировать свое приложение асинхронно. Вы должны запустить «процесс загрузки веб-страницы», а затем вернуться к основному циклу и, как только страница закончит загрузку, вы должны асинхронно опубликовать уведомление, которое обрабатывается в основном потоке и запускает код, который должен запускаться, как только загрузка завершена.

2 голосов
/ 30 сентября 2008

У меня были похожие проблемы при попытке управления NSRunLoops. обсуждение для runMode:beforeDate: на странице ссылок на класс говорит:

Если к циклу выполнения не подключены источники входного сигнала или таймеры, этот метод завершается немедленно; в противном случае он возвращается после обработки первого входного источника или достижения limitDate. Удаление всех известных входных источников и таймеров из цикла выполнения вручную не гарантирует выхода из цикла выполнения. Mac OS X может устанавливать и удалять дополнительные источники ввода для обработки запросов, направленных на поток получателя. Поэтому эти источники могут помешать выходу из цикла выполнения.

Мое лучшее предположение заключается в том, что источник ввода подключен к вашему NSRunLoop, возможно, самой OS X, и что runMode:beforeDate: блокируется до тех пор, пока этот источник ввода не обработает или не удалит какой-либо вход. В вашем случае для этого потребовалось « пара секунд и до 10 секунд », и в этот момент runMode:beforeDate: вернется с логическим значением, while() будет запущен снова, он обнаружит что shouldKeepRunning было установлено на NO, и цикл завершился бы.

С вашим уточнением runMode:beforeDate: вернется в течение 0,1 секунды, независимо от того, подключил он источники входного сигнала или обработал какой-либо вход. Это обоснованное предположение (я не эксперт по внутренним компонентам цикла выполнения), но думаю, что ваше уточнение - верный способ справиться с ситуацией.

0 голосов
/ 14 августа 2013

Ваш второй пример просто работает во время опроса, чтобы проверить ввод цикла выполнения в интервале времени 0,1.

Иногда я нахожу решение для вашего первого примера:

BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...