Идентификационный генератор с локальной статической переменной - ориентированный на многопотоковое исполнение?

Будет следующая часть работы кода как ожидалось в многопоточном сценарии?

int getUniqueID()  
{  
    static int ID=0;  
    return ++ID;  
}

Не необходимо, что идентификаторы, чтобы быть непрерывным - даже если это пропускает значение, это прекрасно. Можно сказать, что, когда эта функция возвращается, возвращенное значение будет уникально через все потоки?

6
задан Paul Stephenson 24 April 2010 в 18:43
поделиться

6 ответов

Примечание. Слово почти используется, потому что глобальная переменная будет инициализирована при запуске процесса (т.е. ее конструктор будет вызываться перед вводом main ), тогда как статическая переменная внутри функции будет инициализирована при первом выполнении оператора.

Ваш вопрос неверен с самого начала:

Генератор идентификаторов с локальной статической переменной - потокобезопасный?

В C / C ++ статическая переменная внутри функции или внутри объявления класса / структуры ведет себя ( почти) как глобальная переменная, а не локальная, основанная на стеке.

Следующий код:

int getUniqueID()  
{  
    static int ID=0;  
    return ++ID;  
}

Будет (почти) похож на псевдокод:

private_to_the_next_function int ID = 0 ;

int getUniqueID()  
{  
    return ++ID;  
}

с псевдоключевым словом private_to_the_next_function , делающим переменную невидимой для всех других функций, кроме getUniqueId ...

Здесь static только скрывает переменную, что делает невозможным доступ к ней из других функций ...

Но даже скрытый, идентификатор переменной остается глобальным: если getUniqueId вызывается несколькими потоками, ID будет таким же потокобезопасным, как и другие глобальные переменные, то есть совсем не поточно-ориентированным .

Редактировать: Время жизни переменных

Прочитав комментарии, я почувствовал, что не достаточно ясно дал свой ответ. Я использую не глобальные / локальные понятия для обозначения их области видимости, а для обозначения их жизненного цикла:

Глобальный объект будет существовать, пока выполняется процесс, а локальный объект, который размещен в стеке, начнет свою жизнь, когда входит в область действия / функцию и перестанет существовать в момент выхода из области действия / функции. Это означает, что глобальное значение сохранит свое значение, а локальное - нет. Это также означает, что глобальный будет совместно использоваться потоками, а локальный - нет.

Добавьте к нему ключевое слово static , которое имеет разное значение в зависимости от контекста (вот почему использование static для глобальных переменных и функций в C ++ не рекомендуется в пользу анонимного пространства имен, но я игнорирую).

При квалификации локальной переменной эта локальная переменная перестает вести себя как локальная. Он становится глобальным, скрытым внутри функции. Таким образом, он ведет себя так, как если бы значение локальной переменной было волшебным образом запомнено между вызовами функции, но в этом нет никакого волшебства: переменная является глобальной и останется «живой» до конца программы.

Вы можете «увидеть» это, зарегистрировав создание и уничтожение объекта, объявленного статическим внутри функции. Конструкция произойдет, когда будет выполнен оператор объявления, и уничтожение произойдет в конце процесса:

bool isObjectToBeConstructed = false ;
int iteration = 0 ;

struct MyObject
{
   MyObject() { std::cout << "*** MyObject::MyObject() ***" << std::endl ; }
   ~MyObject() { std::cout << "*** MyObject::~MyObject() ***" << std::endl ; }
};

void myFunction()
{
   std::cout << "   myFunction() : begin with iteration " << iteration << std::endl ;

   if(iteration < 3)
   {
      ++iteration ;
      myFunction() ;
      --iteration ;
   }
   else if(isObjectToBeConstructed)
   {
      static MyObject myObject ;
   }

   std::cout << "   myFunction() : end with iteration " << iteration << std::endl ;
}


int main(int argc, char* argv[])
{
   if(argc > 1)
   {
      std::cout << "main() : begin WITH static object construction." << std::endl ;
      isObjectToBeConstructed = true ;
   }
   else
   {
      std::cout << "main() : begin WITHOUT static object construction." << std::endl ;
      isObjectToBeConstructed = false ;
   }

   myFunction() ;

   std::cout << "main() : end." << std::endl ;
   return 0 ;
}

Если вы запустите исполняемый файл без параметров, выполнение никогда не будет проходить через статический объект объявления, и поэтому он никогда не будет создан или разрушен, как показано в журналах:

main() : begin WITHOUT static object construction.
   myFunction() : begin with iteration 0
   myFunction() : begin with iteration 1
   myFunction() : begin with iteration 2
   myFunction() : begin with iteration 3
   myFunction() : end with iteration 3
   myFunction() : end with iteration 2
   myFunction() : end with iteration 1
   myFunction() : end with iteration 0
main() : end.

Но если вы запустите его с параметром, то объект будет создан при третьем рекурсивном вызове myFunction и уничтожен только при конец процесса, как видно из журналов:

main() : begin WITH static object construction.
   myFunction() : begin with iteration 0
   myFunction() : begin with iteration 1
   myFunction() : begin with iteration 2
   myFunction() : begin with iteration 3
*** MyObject::MyObject() ***
   myFunction() : end with iteration 3
   myFunction() : end with iteration 2
   myFunction() : end with iteration 1
   myFunction() : end with iteration 0
main() : end.
*** MyObject::~MyObject() ***

Теперь, если вы играете с тем же кодом, но вызываете myFunction через несколько потоков, у вас будут условия гонки в конструкторе myObject. И если вы вызовете эти методы myObject или используете эти переменные myObject в myFunction, вызываемой несколькими потоками, у вас также будут условия гонки.

Таким образом, статическая локальная переменная myObject - это просто глобальный объект, скрытый внутри функции.

0
ответ дан 8 December 2019 в 02:52
поделиться

Если вам просто нужно несколько монотонно возрастающих (или очень близких к нему) чисел в N потоках, рассмотрите это (k - это некоторое число, такое что 2 ^ k> N):

int getUniqueIDBase()  
{  
    static int ID=0;  
    return ++ID;  
}

int getUniqueID()
{
    return getUniqueIDBase() << k + thread_id;
}
3
ответ дан 8 December 2019 в 02:52
поделиться

++ не обязательно является атомарным, поэтому нет, он не является потокобезопасным. Однако многие среды выполнения C предоставляют атомарные версии, например __ sync_add_and_fetch () для gcc и InterlockedIncrement () в Windows.

4
ответ дан 8 December 2019 в 02:52
поделиться

getUniqueID имеет как минимум два состояния гонки. При инициализации ID и при увеличении ID . Я переписал функцию, чтобы более четко отображать расы данных.

int getUniqueID()  
{
   static bool initialized = false;
   static int ID;
   if( !initialized )
   {
      sleep(1);
      initialized = true;

      sleep(1);
      ID = 1;      
   }

   sleep(1);
   int tmp = ID;

   sleep(1);
   tmp += 1;

   sleep(1);
   ID = tmp;

   sleep(1);
   return tmp;
}

Приращение обманчиво, оно выглядит настолько маленьким, что может показаться атомарным. Однако это операция загрузки-изменения-сохранения. Загрузите значение из памяти в регистр ЦП. inc регистр. Сохраните регистр обратно в память.

Используя новый c ++ 0x, вы можете просто использовать тип std :: atomic .

int getUniqueID()  
{  
    static std::atomic<int> ID{0};  
    return ++ID;  
}

ПРИМЕЧАНИЕ: технически я солгал. Нулевые инициализированные глобальные переменные (включая статику функций) могут храниться в памяти bss, и их не нужно инициализировать после запуска программы. Однако приращение по-прежнему остается проблемой.

2
ответ дан 8 December 2019 в 02:52
поделиться

Нет, не будет. Ваш процессор должен будет выполнить следующие шаги для выполнения этого кода:

  • Извлечь значение идентификатора из памяти в регистр
  • Увеличить значение в регистре
  • Сохранить увеличенное значение в памяти

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

  • Поток a извлекает значение 1 в регистр
  • Поток a увеличивает значение на единицу, поэтому регистр теперь содержит 2
  • Переключение контекста
  • Поток b извлекает значение 1 (которое все еще находится в памяти)
  • Переключение контекста
  • Поток a сохраняет 2 в памяти и возвращает
  • Переключение контекста
  • Поток b увеличивает значение, которое он сохранил в своем регистре, до 2
  • Поток b (также) сохраняет значение 2 в памяти и возвращает 2

Итак, оба потока возвращают 2.

18
ответ дан 8 December 2019 в 02:52
поделиться

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

5
ответ дан 8 December 2019 в 02:52
поделиться