Почему мы не можем использовать синхронизацию отправки в текущей очереди?


я столкнулся со сценарием, где у меня был обратный вызов делегата, который мог произойти либо в основном потоке, либо в другом потоке, и я не знал бы, что до времени выполнения (используя 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 51

6 ответов:

Я нашел это в документация (последняя глава):

не вызывайте функцию dispatch_sync из выполняемой задачи в той же очереди, которую вы передаете вызову функции. Это блокировка очереди. Если вам нужно отправить в текущую очередь, сделайте так асинхронно с помощью функции dispatch_async.

кроме того, я перешел по ссылке, которую вы предоставили и в описании dispatch_sync I прочтите это:

вызов этой функции и нацеливание на текущую очередь приводит к взаимоблокировке.

поэтому я не думаю, что это проблема с GCD, я думаю, что единственный разумный подход-это тот, который вы изобрели после обнаружения проблемы.

dispatch_sync делает две вещи:

  1. очереди блок
  2. блокирует текущий поток, пока блок не закончит работу

учитывая, что основной поток является последовательной очередью (что означает, что он использует только один поток), следующее утверждение:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});

вызовет следующие события:

  1. dispatch_sync ставит блок в основную очередь.
  2. dispatch_sync блокирует поток основного очередь до завершения выполнения блока.
  3. 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 выполняется на блоке.

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