Подтвердить что ты не робот

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

Я столкнулся с сценарием, когда у меня был обратный вызов делегата, который мог произойти либо в основном потоке, либо в другом потоке, и я бы не знал, до какого времени он будет работать (используя StoreKit.framework).

У меня также был UI-код, который мне нужно было обновить в этом обратном вызове, который должен был произойти до выполнения функции, поэтому моя первоначальная мысль заключалась в том, чтобы иметь такую ​​функцию:

-(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, или есть что-то, что мне не хватает в документах?

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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, но избегая отправки disp_sync в текущую очередь, поэтому я проверяю тег и действую соответственно. Поскольку get получает иерархию, значение не будет найдено в workerQ, но оно будет найдено в funnelQ. Это способ выяснить, является ли какая-либо очередь в иерархии той, где мы сохранили значение. И, следовательно, для предотвращения отправки диспетчера_синхронизации в текущую очередь.

Если вам интересно о функциях, которые читают/записывают данные контекста, есть три:

  • dispatch_queue_set_specific: записать в очередь.
  • dispatch_queue_get_specific: чтение из очереди.
  • dispatch_get_specific: функция удобства для чтения из текущей очереди.

Ключ сравнивается с указателем и никогда не разыгрывается. Последним параметром в сеттере является деструктор для отпускания ключа.

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

Ответ 3

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

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

Осторожнее, это говорит текущая тема.

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

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

Оптимизация, о которой говорится в этом предложении, касается потоков, а не очередей. Например, у вас есть две последовательные очереди, 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. Видишь проблему? Это классический тупик.

Ответ 4

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

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

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

Ответ 5

Оба 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 решила, что лучше иметь последовательное поведение здесь независимо от очереди.

Ответ 6

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

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