Потоковое ленивое создание с TBB? - PullRequest
1 голос
/ 19 февраля 2012

В моем C ++-коде я храню указатель на объект, который следует создавать лениво, т. Е. Создавать только по запросу.У меня есть следующий код, который явно не является потокобезопасным.

LAZY* get_lazy()
{
    if (0 == _lazy)
        _lazy = create_lazy();
    return _lazy;
}

Интересно, какую синхронизацию мне следует использовать здесь?Я знаю, Boost.thread обеспечивает поддержку для однократной инициализации.Но я надеюсь, что есть простое решение, использующее только TBB + C ++.Я должен также отметить, что ...

  • Я не могу создать _lazy как статический объект (я действительно хочу сохранить неограниченный массив таких лениво созданных объектов)
  • Такие LAZY объекты не могут быть перераспределены (создание очень дорого)

Ответы [ 3 ]

2 голосов
/ 19 февраля 2012

Можно ли иногда звонить create_lazy более одного раза?Если это так, то это очень легкое и эффективное решение, использующее только TBB:

tbb::atomic<LAZY*> lazy;

if(!lazy)
{
    LAZY *newlazy = create_lazy();

    if(lazy.compare_and_swap(newlazy, 0))
    {
        // lazy was initialized elsewhere.
        delete newlazy;
    }
}

// use lazy.

Это будет иметь намного меньше (ноль!) Издержек, чем решение Maciej, но опять-таки будет работать только в том случае, если будет нормально иногда звонить create_lazy более одного раза в случае возникновения разногласий между потоками по этой конкретной переменной.

Один из способов избежать как взаимного исключения, так и вызова create_lazy более одного раза - это использовать спин-цикл.Это будет использовать больше ЦП, чем мьютекс, если есть конфликт, но все равно будут низкие накладные расходы:

tbb::atomic<LAZY*> lazy;
static int sentry;

if(!lazy && !lazy.compare_exchange((LAZY*)&sentry, 0))
{
    // lazy is set to a sentry value while being allocated.
    try{ lazy = create_lazy(); }
    catch(...) { lazy = 0; throw; }
}
else
{
    // yield the thread while lazy is still set to the sentry.
    while(lazy == (LAZY*)&sentry)
    {
        tbb::this_tbb_thread::yield();
    }
}

// use lazy.
2 голосов
/ 21 февраля 2012

Вы также можете посмотреть, как эта проблема решается внутри TBB. Имя для поиска в коде: atomic_do_once; это внутренняя (на момент написания) функция TBB для отложенной инициализации. Определение этой функции и вспомогательного содержимого находится в src / tbb_misc.h, и в других файлах есть несколько мест, где она используется.

Основная идея такая же, как в ответе @ CoryNelson, но обобщена с помощью флага с тремя состояниями (см. enum do_once_state). Нужно создать статическую переменную типа tbb::atomic<do_once_state> и передать ее вместе с функцией / функтором, который должен быть запущен один раз, в вызов atomic_do_once. Например:

void initialize_once();
static tbb::atomic<tbb::internal::do_once_state> init_state;
/*...*/
// Safe to execute concurrently
tbb::internal::atomic_do_once( &initialize_once, init_state );

Для длительной инициализации предпочтительнее использовать tbb::mutex, как рекомендовано @MaciejDopieralski, так как оно позволяет избежать чрезмерной загрузки ЦП, переводя ожидающие потоки в спящий режим. Обратите внимание, что большинство других разновидностей мьютекса в TBB также вращаются, а не спят.

2 голосов
/ 19 февраля 2012

Вам нужен локальный мьютекс (tbb :: mutex), чтобы быть уверенным, что вы создали свой ленивый объект только один раз.

#include <tbb/mutex.h>

tbb::atomic<LAZY*> _lazy;
tbb::mutex myMutex;

LAZY* GetLazy()
{
  if (0 == _lazy)
  {
    myMutex.lock();
    if (0 == _lazy)
        _lazy = create_lazy();
    myMutex.unlock();
  }
  return _lazy;
}
...