Почему мы не можем использовать dispatch_sync в текущей очереди?
Я столкнулся со сценарием, в котором у меня был обратный вызов делегата, который мог произойти либо в основном потоке, либо в другом потоке, и я не знал бы, что до времени выполнения (используя StoreKit.framework
).
У меня также был код пользовательского интерфейса, который мне нужно было обновить в этом обратном вызове, который должен был произойти до того, как функция была выполнена, поэтому моей первоначальной мыслью было иметь такую функцию:
-(void) someDelegateCallback:(id) sender
{
dispatch_sync(dispatch_get_main_queue(), ^{
// ui update code here
});
// code here that depends upon the UI getting updated
}
Это прекрасно работает, когда он выполняется в фоновом потоке. Однако при выполнении в основном потоке программа заходит в тупик.
Это само по себе кажется интересным, если я читаю документы для dispatch_sync
Хорошо, тогда я ожидал бы, что он просто выполнит блок напрямую, не беспокоясь о планировании его в runloop, как сказано здесь:
В качестве оптимизации эта функция вызывает блок в текущем потоке, когда это возможно.
Но это не так уж и сложно, просто нужно больше печатать, что привело меня к такому подходу:
-(void) someDelegateCallBack:(id) sender
{
dispatch_block_t onMain = ^{
// update UI code here
};
if (dispatch_get_current_queue() == dispatch_get_main_queue())
onMain();
else
dispatch_sync(dispatch_get_main_queue(), onMain);
}
Тем не менее, это кажется немного задом наперед. Было ли это ошибкой при создании GCD, или мне чего-то не хватает в документации?
6 ответов
Я нашел это в документации (последняя глава):
Не вызывайте функцию dispatch_sync из задачи, которая выполняется в той же очереди, которую вы передаете в вызов функции. Это приведет к блокировке очереди. Если вам нужно отправить в текущую очередь, сделайте это асинхронно, используя функцию dispatch_async.
Кроме того, я следовал по ссылке, которую вы предоставили, и в описании dispatch_sync я прочитал это:
Вызов этой функции и ориентация на текущую очередь приводят к тупику.
Так что я не думаю, что это проблема с GCD, я думаю, что единственный разумный подход - это тот, который вы изобрели после обнаружения проблемы.
dispatch_sync
делает две вещи:
- поставить блок в очередь
- блокирует текущий поток, пока блок не закончил работу
Учитывая, что основной поток является последовательной очередью (что означает, что он использует только один поток), следующий оператор:
dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});
будет вызывать следующие события:
dispatch_sync
ставит в очередь блок в основной очереди.dispatch_sync
блокирует поток основной очереди, пока блок не завершит выполнение.dispatch_sync
ждет вечно, потому что поток, в котором предполагается запустить блок, заблокирован.
Ключ к пониманию этого заключается в том, что dispatch_sync
не выполняет блоки, а только ставит их в очередь. Выполнение произойдет на будущей итерации цикла выполнения.
Следующий подход:
if (queueA == dispatch_get_current_queue()){
block();
} else {
dispatch_sync(queueA,block);
}
это прекрасно, но имейте в виду, что это не защитит вас от сложных сценариев, включающих иерархию очередей. В этом случае текущая очередь может отличаться от ранее заблокированной очереди, в которую вы пытаетесь отправить свой блок. Пример:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
// dispatch_get_current_queue() is B, but A is blocked,
// so a dispatch_sync(A,b) will deadlock.
dispatch_sync(queueA, ^{
// some task
});
});
});
Для сложных случаев, чтение / запись данных значения ключа в очереди отправки:
dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);
static int kKey;
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ,
&kKey,
(void*)tag,
(dispatch_function_t)CFRelease);
dispatch_sync(workerQ, ^{
// is funnelQ in the hierarchy of workerQ?
CFStringRef tag = dispatch_get_specific(&kKey);
if (tag){
dispatch_sync(funnelQ, ^{
// some task
});
} else {
// some task
}
});
Объяснение:
- Я создаю
workerQ
очередь, которая указывает наfunnelQ
очередь. В реальном коде это полезно, если у вас есть несколько "рабочих" очередей, и вы хотите возобновить / приостановить все сразу (что достигается путем возобновления / обновления их целиfunnelQ
очередь). - Я могу направить свои рабочие очереди в любой момент времени, поэтому, чтобы узнать, направляются ли они или нет, я отмечаю
funnelQ
со словом "воронка". - Вниз по дороге я
dispatch_sync
что-тоworkerQ
и по какой-то причине я хочуdispatch_sync
вfunnelQ
, но избегая dispatch_sync в текущей очереди, поэтому я проверяю тег и действую соответственно. Поскольку get идет вверх по иерархии, значение не будет найдено вworkerQ
но это будет найдено вfunnelQ
, Это способ выяснить, является ли какая-либо очередь в иерархии той, где мы сохранили значение. И, следовательно, для предотвращения dispatch_sync в текущей очереди.
Если вам интересно узнать о функциях, которые читают / записывают данные контекста, есть три:
dispatch_queue_set_specific
: Запись в очередь.dispatch_queue_get_specific
: Чтение из очереди.dispatch_get_specific
: Удобная функция для чтения из текущей очереди.
Ключ сравнивается по указателю и никогда не разыменовывается. Последний параметр в установщике - это деструктор для освобождения ключа.
Если вас интересует "указание одной очереди на другую", это означает именно это. Например, я могу указать очередь A на основную очередь, и это приведет к запуску всех блоков в очереди A в основной очереди (обычно это делается для обновлений пользовательского интерфейса).
Я знаю, откуда твое замешательство:
В качестве оптимизации эта функция вызывает блок в текущем потоке, когда это возможно.
Осторожнее, это говорит текущая тема.
Тема!= Очередь
Очередь не владеет потоком, и поток не связан с очередью. Есть темы и есть очереди. Всякий раз, когда очередь хочет запустить блок, ей нужен поток, но это не всегда будет один и тот же поток. Ему просто нужен какой-то поток (каждый раз он может отличаться), и когда он завершает выполнение блоков (на данный момент), тот же поток теперь может использоваться другой очередью.
Оптимизация, о которой говорится в этом предложении, касается потоков, а не очередей. Например, у вас есть две последовательные очереди, QueueA
а также QueueB
и теперь вы делаете следующее:
dispatch_async(QueueA, ^{
someFunctionA(...);
dispatch_sync(QueueB, ^{
someFunctionB(...);
});
});
когда QueueA
запускает блок, он временно владеет потоком, любым потоком. someFunctionA(...)
будет выполняться в этом потоке. Теперь, делая синхронную рассылку, QueueA
больше ничего не может сделать, он должен ждать окончания отправки. QueueB
с другой стороны, также потребуется поток для запуска своего блока и выполнения someFunctionB(...)
, Так что либо QueueA
временно приостанавливает свою нить и QueueB
использует какой-то другой поток для запуска блока или QueueA
протягивает нить QueueB
(в конце концов это все равно не понадобится, пока синхронная отправка не закончится) и QueueB
напрямую использует текущий поток QueueA
,
Само собой разумеется, что последний вариант намного быстрее, так как не требуется переключение потоков. И это оптимизация, о которой говорится в предложении. Так что dispatch_sync()
в другую очередь не всегда может вызвать переключение потока (другая очередь, может быть, тот же поток).
Но dispatch_sync()
все еще не может произойти с той же очередью (тот же поток, да, та же очередь, нет). Это связано с тем, что очередь будет выполнять блок за блоком, и когда она в настоящий момент выполняет блок, она не выполнит другой, пока этот не будет завершен. Так что выполняет BlockA
а также BlockA
делает dispatch_sync()
из BlockB
в той же очереди. Очередь не запускается BlockB
пока он еще работает BlockA
, но работает BlockA
не будет продолжаться до BlockB
побежал. Видишь проблему?
В документации четко указано, что прохождение текущей очереди вызовет тупик.
Теперь они не говорят, почему они спроектировали вещи таким образом (за исключением того, что на самом деле потребовался бы дополнительный код, чтобы заставить его работать), но я подозреваю, что причина для того, чтобы делать вещи таким образом, заключается в том, что в этом особом случае блоки будут "прыгать" очередь, т. е. в нормальных случаях ваш блок завершает работу после того, как все другие блоки в очереди запущены, но в этом случае он будет выполняться раньше.
Эта проблема возникает, когда вы пытаетесь использовать GCD как механизм взаимного исключения, и этот конкретный случай эквивалентен использованию рекурсивного мьютекса. Я не хочу вступать в спор о том, лучше ли использовать GCD или традиционный API взаимного исключения, такой как мьютексы pthreads, или даже о том, стоит ли использовать рекурсивные мьютексы; Я позволю другим спорить по этому поводу, но на это, безусловно, есть спрос, особенно когда это основная очередь, с которой вы имеете дело.
Лично я думаю, что dispatch_sync был бы более полезен, если бы он поддерживал это или если была другая функция, обеспечивающая альтернативное поведение. Я хотел бы призвать других, которые так думают, подать отчет об ошибке в Apple (как я уже сделал, ID: 12668073).
Вы можете написать свою собственную функцию, чтобы сделать то же самое, но это немного хак:
// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
if (dispatch_get_specific (queue))
block ();
else
dispatch_sync (queue, block);
}
NB Раньше у меня был пример, который использовал dispatch_get_current_queue(), но теперь это устарело.
И то и другое dispatch_async
а также dispatch_sync
выполнить толчок их действия в нужной очереди. Действие не происходит сразу; это произойдет на некоторой будущей итерации цикла выполнения очереди. Разница между dispatch_async
а также dispatch_sync
в том, что dispatch_sync
блокирует текущую очередь, пока действие не завершится.
Подумайте о том, что происходит, когда вы выполняете что-то асинхронно в текущей очереди. Опять же, это не происходит сразу; он помещает его в очередь FIFO и должен дождаться окончания текущей итерации цикла выполнения (и, возможно, также дождаться других действий, которые были в очереди, прежде чем включить это новое действие).
Теперь вы можете спросить, при асинхронном выполнении действия в текущей очереди, почему бы не всегда просто вызывать функцию напрямую, а не ждать до некоторого будущего времени. Ответ в том, что между ними есть большая разница. Часто вам нужно выполнить действие, но оно должно быть выполнено после того, как какие-либо побочные эффекты выполняются функциями в стеке в текущей итерации цикла выполнения; или вам нужно выполнить свое действие после некоторого анимационного действия, которое уже запланировано в цикле выполнения и т. д. Вот почему много раз вы будете видеть код [obj performSelector:selector withObject:foo afterDelay:0]
(да, это отличается от [obj performSelector:selector withObject:foo]
).
Как мы уже говорили, dispatch_sync
такой же как dispatch_async
за исключением того, что он блокируется, пока действие не будет завершено. Таким образом, очевидно, почему он заблокирован - блок не может быть выполнен до тех пор, пока не закончится текущая итерация цикла выполнения; но мы ждем его окончания, прежде чем продолжить.
Теоретически можно было бы сделать особый случай для dispatch_sync
когда это текущий поток, чтобы выполнить его немедленно. (Такой особый случай существует для performSelector:onThread:withObject:waitUntilDone:
когда поток является текущим потоком и waitUntilDone:
Если ДА, он выполняет его немедленно.) Однако, я думаю, Apple решила, что лучше иметь последовательное поведение здесь независимо от очереди.
Нашел из следующей документации. https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html
В отличие от dispatch_async, функцияdispatch_sync не возвращается, пока блок не закончится. Вызов этой функции и ориентация на текущую очередь приводят к тупику.
В отличие от dispatch_async, сохранение не выполняется в целевой очереди. Поскольку вызовы этой функции являются синхронными, она "заимствует" ссылку вызывающей стороны. Кроме того, Block_copy не выполняется для блока.
В качестве оптимизации эта функция вызывает блок в текущем потоке, когда это возможно.