Подтвердить что ты не робот

Лучший способ заставить NSRunLoop ждать установки флага?

В документации 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 секунды.

Любые идеи, почему исходный код демонстрирует такое поведение?

4b9b3361

Ответ 1

Runloops может быть немного волшебным полем, где только что происходит.

В основном вы говорите runloop, чтобы обработать некоторые события, а затем вернуться. ИЛИ верните, если он не обработает какие-либо события до того, как будет удален тайм-аут.

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

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

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

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

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

Ответ 2

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

@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

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

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

Ответ 3

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

Если вы хотите запустить модально - например, показывая панель прогресса - запустите модально! Идем дальше и используем методы 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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

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