NSRunLoop's runMode:beforeDate: - правильный подход для установки beforeDate

У меня есть сомнения относительно правильного использования метода NSRunLoop runMode:beforeDate.

У меня есть вторичный фоновый поток, который обрабатывает сообщения делегатов по мере их получения.

По сути, у меня есть интенсивная логика процесса, которую необходимо выполнять в фоновом потоке.

Итак, у меня есть 2 объекта, ObjectA и AnotherObjectB.

ObjectA инициализирует AnotherObjectB и говорит AnotherObjectB начать делать свое дело. AnotherObjectB работает асинхронно, поэтому ObjectA действует как делегат AnotherObjectB. Теперь код, который необходимо выполнить в сообщениях делегата, необходимо выполнить в фоновом потоке. Итак, для ObjectA я создал NSRunLoop и сделал что-то вроде этого, чтобы настроить цикл выполнения:

do {
 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (aCondition);

Где aCondition устанавливается где-то в «сообщении делегата о завершении».

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

У меня вопрос: правильный ли это подход?

Причина, по которой я спрашиваю об этом, заключается в том, что [NSDate distantFuture] — это дата, охватывающая пару столетий. Таким образом, runLoop не истечет время ожидания до «distantFuture» — до тех пор я определенно не буду использовать свой Mac или эту версию iOS. >_‹

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

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

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

Итак, заранее спасибо за всю вашу помощь!

Ваше здоровье!


person codeBearer    schedule 09.11.2012    source источник


Ответы (2)


Код в документах :

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

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

где shouldKeepRunning устанавливается в NO где-то еще в программе.

После вашего последнего сообщения снимите shouldKeepRunning (в том же потоке, что и цикл выполнения!), И он должен завершиться. Ключевая идея здесь заключается в том, что вам нужно отправить циклу выполнения событие, чтобы он знал, что нужно остановиться.

(Также обратите внимание, что NSRunLoop не является потокобезопасным; я думаю, вы должны использовать -[NSObject performSelector:onThread:...].)

В качестве альтернативы, если это работает для ваших целей, используйте фон очереди отправки/NOperationQueue (но обратите внимание, что код, который делает это, не должен касаться цикла выполнения; такие вещи, как запуск NSURLConnection из очереди отправки/рабочего потока NSOperationQueue, скорее всего, вызовут проблемы ).

person tc.    schedule 09.11.2012
comment
Спасибо за ответ. Код, который я реализовал, в значительной степени делает то же самое, что вы упомянули, - единственное, что он не делает, это проверяет возвращаемое значение BOOL runMode:beforeDate:. Кроме того, спасибо за поточно-безопасный заголовок. Доступ к экземпляру NSRunLoop должен осуществляться только из потока, которому он принадлежит, а не из других потоков. В моем случае использование perfromSelector:onThread: нецелесообразно, так как я использую объект делегата. Из вашего ответа я думаю, что у меня может быть правильная реализация. Еще раз спасибо. :) - person codeBearer; 10.11.2012
comment
Вы должны быть немного осторожны: -runMode:beforeDate: возвращает NO немедленно, если нет источников цикла выполнения (аналогично -run). Это нормально, если вы можете гарантировать, что всегда есть хотя бы один источник (в качестве альтернативы, удаление всех источников может быть способом вернуть -run?) - person tc.; 14.11.2012
comment
Извините, но ответ пока не ясен и думаю тоже немного неверен. - person Motti Shneor; 05.10.2013
comment
Извините, все еще неясно. AFAIK сброс флага shouldKeepRunning является атомарным и может быть выполнен из любого потока. Смысл в том, чтобы позволить произвольному пользователю сигнализировать о завершении потока, владеющего RL. Цикл выполнения должен обработать еще одно событие, чтобы поймать изменение флага, но я НЕ понимаю, какого рода события будет достаточно. Документ NSRunLoop меня смущает. Что это за источники ввода, которые отправляют события в цикле выполнения? Является ли такое событие обычным сообщением Obj-C (т. е. [self PerformSelector:@selector(dummy) onThread:runloopOwningThread])? - person Motti Shneor; 05.10.2013
comment
Поскольку вам все равно нужно обработать событие, вы можете сделать [foo performSelector:@selector(setShouldKeepRunningToFalse) onThread:backgroundThread ...]. В Java вы можете использовать volatile boolean, чтобы получить гарантии атомарности и свежести, но C предлагает очень мало гарантий при использовании потоков (и, аналогично, но по-другому, обработчиков сигналов). Однако NSOperationQueue будет легче сделать правильно. - person tc.; 20.10.2013

Причина, по которой я спрашиваю об этом, заключается в том, что [NSDate DistanceFuture] — это дата, охватывающая пару столетий.

Метод runMode:beforeDate: будет

  • немедленно вернуть NO, если нет источников, запланированных на RunLoop.

  • возвращать YES всякий раз, когда событие было обработано.

  • вернуть YES, когда будет достигнуто limitDate.

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

Подумайте о получении пакетов с тайм-аутом из сетевого сокета. Вызов fetch возвращается, когда приходит пакет или когда истекает тайм-аут. Тем не менее, если вы хотите обработать следующий пакет, вы должны снова вызвать метод выборки.

Следующий код, к сожалению, довольно плохой по двум причинам:

// BAD CODE! DON'T USE!
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) {
    [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
}
  1. Если RunLoopSource еще не запланировано для RunLoop, он будет тратить 100% процессорного времени, так как метод вернется сразу же, чтобы вызвать его снова, и так быстро, как ЦП сможет это сделать.

  2. AutoreleasePool никогда не продлевается. Объекты, которые автоматически освобождаются (и даже ARC делает это), добавляются в текущий пул, но никогда не освобождаются, поскольку пул никогда не очищается, поэтому потребление памяти будет увеличиваться, пока выполняется этот цикл. Насколько многое зависит от того, что на самом деле делают ваши RunLoopSources и как они это делают.

Лучшей версией будет:

// USE THIS INSTEAD
NSDate * distFuture = NSDate.distantFuture;
NSRunLoop * runLoop = NSRunLoop.currentRunLoop;
while (keepRunning) @autoreleasepool {
    BOOL didRun = [runLoop runMode:NSDefaultRunLoopMode beforDate:distFuture];
    if (!didRun) usleep(1000);
}

Он решает обе проблемы:

  • AutoreleasePool создается при первом запуске цикла и очищается после каждого запуска, поэтому потребление памяти со временем не увеличивается.

  • В случае, если RunLoop на самом деле вообще не запустился, текущий поток приостанавливается на одну миллисекунду перед новой попыткой. Таким образом, загрузка ЦП будет довольно низкой, поскольку, поскольку RunLoopSource не установлено, этот код выполняется только один раз в миллисекунду.

Чтобы надежно завершить цикл, вам нужно сделать две вещи:

  1. Установите keepRunning на NO. Обратите внимание, что вы должны объявить keepRunning как volatile! Если вы этого не сделаете, компилятор может оптимизировать проверку и превратить ваш цикл в бесконечный цикл, поскольку он не видит кода в текущем контексте выполнения, который когда-либо мог бы изменить переменную, и он не может знать, что какой-то другой код где-то еще ( а может и в другой ветке) может в фоновом режиме изменить. Вот почему в этих случаях вам обычно нужен барьер памяти (блокировка, мьютекс, семафор или атомарная операция), поскольку компиляторы не оптимизируют эти барьеры. Однако в этом простом случае достаточно использовать volatile, так как BOOL всегда является атомарным в Obj-C, а volatile говорит компилятору Всегда проверять значение этой переменной, так как оно может измениться за вашей спиной, и вы не увидите этого изменения при компиляции. время.

  2. Если переменная была изменена из другого потока, а не из обработчика событий, ваш поток RunLoop может находиться в спящем режиме внутри вызова runMode:beforeDate:, ожидая возникновения события RunLoopSource, которое может занять любое время или вообще никогда не произойти. Чтобы заставить этот вызов вернуться немедленно, просто запланируйте событие после изменения переменной. Это можно сделать с помощью performSelector:onThread:withObject:waitUntilDone:, как показано ниже. Выполнение этого селектора считается событием RunLoop, и метод вернется после вызова селектора, увидит, что переменная изменилась, и выйдет из цикла.

volatile BOOL keepRunning;

- (void)wakeMeUpBeforeYouGoGo {
    // Jitterbug
}

// ... In a Galaxy Far, Far Away ...
    keepRunning = NO;
    [self performSelector:@selector(wakeMeUpBeforeYouGoGo) 
        onThread:runLoopThread withObject:nil waitUntilDone:NO];

person Mecki    schedule 30.04.2021