Почему мы не можем использовать синхронизацию отправки в текущей очереди?
я столкнулся со сценарием, где у меня был обратный вызов делегата, который мог произойти либо в основном потоке, либо в другом потоке, и я не знал бы, что до времени выполнения (используя 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);
}
однако, это кажется немного назад. Была ли это ошибка в НОД, или есть что-то, что мне не хватает в документации?
6 ответов:
Я нашел это в документация (последняя глава):
не вызывайте функцию dispatch_sync из выполняемой задачи в той же очереди, которую вы передаете вызову функции. Это блокировка очереди. Если вам нужно отправить в текущую очередь, сделайте так асинхронно с помощью функции dispatch_async.
кроме того, я перешел по ссылке, которую вы предоставили и в описании dispatch_sync I прочтите это:
вызов этой функции и нацеливание на текущую очередь приводит к взаимоблокировке.
поэтому я не думаю, что это проблема с 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 в текущую очередь, поэтому я проверяю тег и действую соответственно. Потому что вам идет вверх по иерархии, значение не будет найдено в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()
в другую очередь не всегда может вызвать переключатель потока (другая очередь, возможно, тот же поток).но a
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); }
N. B. Ранее у меня был пример, который использовал dispatch_get_current_queue() но это теперь он устарел.
и
dispatch_async
иdispatch_sync
выполнить push их действие на нужную очередь. Действие происходит не сразу; оно происходит на некоторой будущей итерации цикла выполнения очереди. Разница между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#//apple_ref/c/func/dispatch_sync
в отличие от dispatch_async,"dispatch_sync" функция не возвращается, пока блок не будет завершен. Вызов этой функции и нацеливание на текущую очередь приводит к взаимоблокировке.
в отличие от dispatch_async, не сохранить выполняется в целевой очереди. Поскольку вызовы этой функции являются синхронными, это"занимает" справочник абонента. Более того, нет Block_copy выполняется на блоке.
в качестве оптимизации, эта функция вызывает блок в текущем потоке, когда это возможно.