Можно ли объявить dispatch_once_t предикат как переменную-член вместо статического?

Я хочу запустить блок кода только один раз для каждого экземпляра.

Можно ли объявить dispatch_once_t предикат как переменную-член вместо статической переменной?

Из GCD Reference мне это не ясно.

Предикат должен указывать на переменную, хранящуюся в глобальной или статической объем. Результат использования предиката с автоматическим или динамическим хранилище undefined.

Я знаю, что могу использовать dispatch_semaphore_t и логический флаг, чтобы сделать то же самое. Мне просто интересно.

Ответ 1

dispatch_once_t не должна быть переменной экземпляра.

Для реализации dispatch_once() требуется, чтобы значение dispatch_once_t равно нулю, а никогда не было ненулевым. Для случая, которому ранее не нужен нуль, для обеспечения правильной работы потребуются дополнительные барьеры памяти, но dispatch_once() опускает эти барьеры по причинам производительности.

Переменные экземпляра инициализируются до нуля, но их память может иметь ранее сохраненное другое значение. Это делает их небезопасными для использования dispatch_once().

Ответ 2

Обновление 16 ноября

Этот вопрос был первоначально возвращен в 2012 году с "развлечением", он не претендовал на предоставление окончательного ответа и не дал оговорки в этом отношении. Оглядываясь назад, такое развлечение должно было остаться частным, хотя некоторые наслаждались этим.

В августе 2016 года это Q & A было привлечено к моему вниманию, и я дал правильный ответ. В этом написано:

Я собираюсь, по-видимому, не соглашаться с Грегом Паркером, но, вероятно, не очень...

Ну, похоже, что мы с Грегом не согласны с тем, не согласны ли мы, или ответ, или что-то в этом роде;-) Поэтому я обновляю свой ответ 2016 2016 года с более подробной базой ответа, почему это может быть неправильно, и если да, то как это исправить (так что ответ на исходный вопрос по-прежнему "да" ). Надеюсь, Грег и я либо согласимся, либо я кое-что узнаю - результат хороший!

Итак, сначала ответ 16 августа, как и было, затем объяснение основы для ответа. Исходное развлечение было удалено, чтобы избежать путаницы, студенты истории могут просматривать тропу редактирования.


Ответ: август 2016

Я собираюсь, по-видимому, не соглашаться с Грегом Паркером, но, вероятно, не очень...

Оригинальный вопрос:

Можно ли объявить предикат dispatch_once_t как переменную-член вместо статической переменной?

Короткий ответ: ответ "да" ПРЕДОСТАВЛЯЕТСЯ, существует барьер памяти между первоначальным созданием объекта и любым использованием dispatch_once.

Краткое пояснение: Требование к переменной dispatch_once_t для dispatch_once состоит в том, что она должна быть изначально нулевой. Трудность исходит из операций переопределения памяти на современных многопроцессорных системах. Хотя может показаться, что хранилище в местоположении было выполнено в соответствии с текстом программы (языком высокого уровня или уровнем ассемблера), фактическое хранилище может быть переупорядочено и произойдет после последующего чтения того же места. Чтобы устранить эту проблему, можно использовать барьеры памяти, которые заставляют все операции с памятью, возникающие перед ними, заполняться до тех, которые следуют за ними. Apple предоставляет OSMemoryBarrier() для этого.

С dispatch_once Apple заявляет, что нулевые инициализированные глобальные переменные гарантированно равны нулю, но что нулевые инициализированные переменные экземпляра (и инициализация нуля - это значение по умолчанию Objective-C) не гарантируются равными нулю до того, как Выполняется dispatch_once.

Решение состоит в том, чтобы вставить барьер памяти; в предположении, что dispatch_once встречается в некотором методе члена экземпляра, очевидное место для размещения этого барьера памяти находится в методе init, поскольку (1) он будет выполняться только один раз (на каждый экземпляр) и (2) init должен быть возвращен до вызова любого другого метода-члена.

Итак, да, с соответствующим барьером памяти, dispatch_once может использоваться с переменной экземпляра.


Ноябрь 2016

Преамбула: примечания к dispatch_once

Эти примечания основаны на коде Apple и комментариях для dispatch_once.

Использование dispatch_once следует стандартным шаблонам:

id cachedValue;
dispatch_once_t predicate = 0;
...
dispatch_once(&predicate, ^{ cachedValue = expensiveComputation(); });
... use cachedValue ...

а две последние строки расширены (dispatch_once - это макрос), например:

if (predicate != ~0) // (all 1's, indicates the block has been executed)  [A]
{
    dispatch_once_internal(&predicate, block);                         // [B]
}
... use cachedValue ...                                                // [C]

Примечания:

  • Источник Apple утверждает, что predicate должен быть инициализирован до нуля и отмечает, что глобальные и статические переменные по умолчанию инициализируются нулями.

  • Обратите внимание, что в строке [A] нет барьера памяти. На процессоре со спекулятивным прогнозом чтения и ветвления в строке [A] может произойти считывание cachedValue в строке [C] до чтения predicate, что может привести к неправильным результатам (неправильное значение для cachedValue)

  • Для предотвращения этого может быть использован барьер, однако это происходит медленно, и Apple хочет, чтобы это было быстрым в общем случае, когда один блок уже был выполнен, поэтому...

  • dispatch_once_internal, строка [B], которая использует внутренние барьеры и атомарные операции, использует специальный барьер dispatch_atomic_maximally_synchronizing_barrier(), чтобы победить спекулятивный режим чтения вперед, и поэтому позвольте линии [A] быть свободным от барьеров и, следовательно, быстро.

  • Выполняется любая работающая с процессором линия [A] перед dispatch_once_internal() и мутированная predicate должна читать 0 из predicate. Использование глобальной или статической инициализации до нуля для predicate гарантирует это.

Важным отрывом для наших текущих целей является то, что dispatch_once_internal мутирует predicate таким образом, что строка [A] работает без какого-либо барьера.

Длинное пояснение к августу 16 Ответ:

Таким образом, мы знаем, что использование глобальной или статической инициализации до нуля отвечает требованиям dispatch_once() безбарьерной скорости. Мы также знаем, что мутации, сделанные от dispatch_once_internal() до predicate, корректно обрабатываются.

Нам нужно определить, можем ли мы использовать переменную экземпляра для predicate и инициализировать ее таким образом, чтобы строка [A] выше никогда не могла прочитать ее предварительно инициализированное значение - как будто это может сломаться.

Мой ответ 16 августа говорит, что это возможно. Чтобы понять основы этого, нам нужно рассмотреть программу и поток данных в многопроцессорной среде со спекулятивным чтением вперед.

Схема выполнения ответа 16 августа и потока данных:

Processor 1                              Processor 2
0. Call alloc
1. Zero instance var used for predicate
2. Return object ref from alloc
3. Call init passing object ref
4. Perform barrier
5. Return object ref from init
6. Store or send object ref somewhere
                           ...
                                         7. Obtain object ref
                                         8. Call instance method passing obj ref
                                         9. In called instance method dispatch_once
                                            tests predicate, This read is dependent
                                            on passed obj ref.

Чтобы иметь возможность использовать переменную экземпляра в качестве предиката, тогда выполнение шага 9 должно быть невозможным таким образом, чтобы он считывал значение в памяти до того, как шаг 1 обнулял его.

Если шаг 4 опущен, т.е. в init не вставлен соответствующий барьер, то, хотя процессор 2 должен получить правильное значение для ссылки на объект, сгенерированное процессором 1, прежде чем он сможет выполнить этап 9, он (теоретически) возможно, что нулевые записи Процессора 1 на шаге 1 еще не были выполнены/записаны в глобальную память, а Процессор 2 их не увидит.

Итак, мы вставляем шаг 4 и выполняем барьер.

Однако теперь мы должны рассмотреть спекулятивный режим чтения-вперед, так же, как dispatch_once(). Может ли процессор 2 выполнить чтение шага 9 до того, как барьер на шаге 4 обеспечит нулевую память?

Рассмотрим:

  • Процессор 2 не может выполнять, спекулятивно или иначе, читать на шаге 9 до тех пор, пока он не получит ссылку на объект, полученную на этапе 7, - и для этого спекулятивно требуется, чтобы процессор определил, что вызов метода на этапе 8, назначение в Objective-C динамически определяется, будет заканчиваться в методе, содержащем этап 9, который является довольно продвинутой (но не невозможной) спекуляцией;

  • Шаг 7 не может получить ссылку на объект до тех пор, пока не будет сохранен/пройден этап 6;

  • Шаг 6 не получил его для хранения/передачи до тех пор, пока не вернется его пятый шаг; и

  • Шаг 5 после барьера на этапе 4...

TL; DR: как на шаге 9 есть ссылка на объект, требуемая для выполнения чтения, пока после шага 4, содержащего барьер? (И учитывая длинный путь выполнения, с несколькими ветвями, некоторые условные (например, внутри метода отправки), является спекулятивным читать-вперед проблема вообще?)

Таким образом, я утверждаю, что барьер на шаге 4 является достаточным даже при наличии спекулятивного шага 9 с продвижением вперед.

Рассмотрение комментариев Грега:

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

Переменные экземпляра инициализируются равными нулю во время создания объекта, а память, которую они занимают, может быть не равна нулю до этого. Однако, как было сказано выше, подходящий барьер может быть использован для обеспечения того, чтобы dispatch_once() не читал значение предварительной инициализации. Я думаю, что Грег не согласен с моим аргументом, если я правильно следую его комментариям и утверждаю, что барьер на шаге 4 недостаточен для обработки спекулятивного чтения.

Предположим, что Грег прав (что совсем не невероятно!), то мы находимся в ситуации, о которой Apple уже говорила в dispatch_once(), нам нужно победить вперед. Apple делает это с помощью барьера dispatch_atomic_maximally_synchronizing_barrier(). Мы можем использовать этот же барьер на шаге 4 и не допустить выполнения следующего кода до тех пор, пока не будет побеждено все возможные умозрительные чтения с процессором 2; и поскольку следующий код, шаги 5 и 6, должен выполняться до того, как процессор 2 даже имеет ссылку на объект, которую он может использовать для спекулятивного выполнения шага 9, все работает.

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

Итак, да, с соответствующим барьером памяти, dispatch_once может использоваться с переменной экземпляра.

Я уверен, что Грег или какой-нибудь другой читатель скажут мне, если я ошибаюсь в своей логике. Я готов к facepalm!

Конечно, вам нужно решить, стоит ли стоимость соответствующего барьера в init, пользуясь тем, что вы получаете от использования dispatch_once(), чтобы получить одноразовое поведение, или же вы должны обращаться к своим требованиям по-другому - и такие альтернативы выходят за рамки этого ответа!

Код для dispatch_atomic_maximally_synchronizing_barrier():

Определение dispatch_atomic_maximally_synchronizing_barrier(), адаптированное из источника Apple, которое вы можете использовать в своем собственном коде:

#if defined(__x86_64__) || defined(__i386__)
   #define dispatch_atomic_maximally_synchronizing_barrier() \
      ({ unsigned long _clbr; __asm__ __volatile__( "cpuid" : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory"); })
#else
   #define dispatch_atomic_maximally_synchronizing_barrier() \
      ({ __c11_atomic_thread_fence(dispatch_atomic_memory_order_seq_cst); })
#endif

Если вы хотите узнать, как это работает, прочитайте исходный код Apple.

Ответ 3

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