it-swarm.com.ru

Почему мы не можем использовать 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, или мне чего-то не хватает в документации?

56
Richard J. Ross III

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

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

Кроме того, я перешел по указанной вами ссылке, и в описании dispatch_sync я прочитал это:

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

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

50
lawicko

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 словом "funnel".
  • В дальнейшем я 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 в основной очереди (обычно это делается для обновлений пользовательского интерфейса).

69
Jano

Я знаю, откуда твое замешательство:

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

Осторожно, говорит текущий поток .

Тема! = Очередь

Очередь не имеет потока, и поток не привязан к очереди. Есть темы и есть очереди. Всякий раз, когда очередь хочет запустить блок, ей нужен поток, но это не всегда будет один и тот же поток. Ему просто нужен какой-то поток (каждый раз он может быть другим), и когда он завершает выполнение блоков (на данный момент), тот же поток теперь может использоваться другой очередью.

Оптимизация, о которой говорится в этом предложении, касается потоков, а не очередей. Например. Предположим, у вас есть две последовательные очереди, QueueA и QueueB, и теперь вы делаете следующее:

dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});

Когда QueueA запускает блок, он временно владеет потоком, любым потоком. someFunctionA(...) будет выполняться в этом потоке. Теперь при выполнении синхронной отправки QueueA больше ничего не может делать, она должна ждать завершения отправки. QueueB, с другой стороны, также потребуется поток для запуска своего блока и выполнения someFunctionB(...). Так что либо QueueA временно приостанавливает свой поток, а QueueB использует какой-то другой поток для запуска блока, либо QueueA передает свой поток QueueB (в конце концов, в любом случае он ему не понадобится до тех пор, пока не завершится синхронная отправка), а QueueB напрямую использует текущий поток QueueA.

Само собой разумеется, что последний вариант намного быстрее, так как не требуется переключение потоков. И this - это оптимизация, о которой говорится в предложении. Таким образом, функция dispatch_sync() в другой очереди не всегда может вызвать переключение потока (другая очередь, возможно, один и тот же поток).

Но dispatch_sync() по-прежнему не может произойти с той же очередью (тот же поток, да, та же очередь, нет). Это связано с тем, что очередь будет выполнять блок за блоком, и когда она в настоящий момент выполняет блок, она не будет выполнять другой, пока не будет выполнен текущий выполнение. Поэтому он выполняет BlockA, а BlockA выполняет dispatch_sync() из BlockB в той же очереди. Очередь не будет работать BlockB, пока она все еще работает BlockA, но выполнение BlockA не будет продолжаться до тех пор, пока не будет запущено BlockB. Видишь проблему? Это классический тупик.

14
Mecki

В документации четко указано, что прохождение текущей очереди вызовет тупик.

Теперь они не говорят, почему они спроектировали вещи таким образом (за исключением того, что на самом деле потребовался бы дополнительный код, чтобы заставить его работать), но я подозреваю, что причина для того, чтобы делать вещи таким образом, заключается в том, что в этом особом случае блоки будут "прыгать" очередь, т. е. в нормальных случаях ваш блок завершает работу после того, как все другие блоки в очереди запущены, но в этом случае он будет выполняться раньше.

Эта проблема возникает, когда вы пытаетесь использовать 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);
}

Нотабене Раньше у меня был пример, который использовал dispatch_get_current_queue (), но теперь это устарело.

6
Chris Suter

И 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: равен YES, он выполняет его немедленно.) Однако, я думаю, Apple решил, что здесь лучше иметь согласованное поведение независимо от очередь.

4
newacct

Нашел из следующей документации. 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 .

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

2
arango_86