Атомный счетчик в gcc

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

Каков правильный способ применения атомного счетчика в GCC?

то есть. Мне нужен счетчик, который работает от нуля до 4 и является потокобезопасным.

Я делал это (это уже завернуто в класс, но не здесь)

static volatile int _count = 0;
const int limit = 4;

int get_count(){
  // Create a local copy of diskid
  int save_count = __sync_fetch_and_add(&_count, 1);
  if (save_count >= limit){
      __sync_fetch_and_and(&_count, 0); // Set it back to zero
  }
  return save_count;
}

Но он работает от 1 до 1 - 4 включительно, а затем около нуля.
Он должен идти от 0 до 3. Обычно я делаю счетчик с оператором мод, но я не знайте, как сделать это безопасно.

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

int get_count(){
   // Create a local copy of diskid
   int save_count = _count;
   if (save_count >= limit){
      __sync_fetch_and_and(&_count, 0); // Set it back to zero
      return 0;
   }

   return save_count;
 }

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

Ответ 1

Ваш код не является атомарным (а второй get_count даже не увеличивает значение счетчика)!

Скажите count - 3 в начале и два потока одновременно вызывают get_count. Один из них получит его атомное добавление, сделанное первым, и увеличит count до 4. Если второй поток будет достаточно быстрым, он может увеличить его до 5 до того, как первый поток сбросит его до нуля.

Кроме того, в вашей обработке wraparound вы reset count равны 0, но не save_count. Это явно не то, что предназначено.

Это проще всего, если limit является степенью 2. Не делайте сокращения самостоятельно, просто используйте

return (unsigned) __sync_fetch_and_add(&count, 1) % (unsigned) limit;

или, альтернативно,

return __sync_fetch_and_add(&count, 1) & (limit - 1);

Это только одна атомная операция за вызов, является безопасной и очень дешевой. Для общих ограничений вы все равно можете использовать %, но это приведет к поломке последовательности, если счетчик когда-либо переполняется. Вы можете попробовать использовать 64-битное значение (если ваша платформа поддерживает 64-битную атомизацию), и просто надейтесь, что она никогда не переполнится; это плохая идея. Правильный способ сделать это - использовать операцию атомного сравнения. Вы делаете это:

int old_count, new_count;
do {
  old_count = count;
  new_count = old_count + 1;
  if (new_count >= limit) new_count = 0; // or use %
} while (!__sync_bool_compare_and_swap(&count, old_count, new_count));

Этот подход обобщает и более сложные последовательности и операции обновления.

Тем не менее, этот тип бесконтактной операции сложно понять, в какой-то степени полагается на поведение undefined (все текущие компиляторы получают это право, но ни один стандарт C/С++ до того, как С++ 0x на самом деле хорошо знает, определенную модель памяти) и легко сломается. Я рекомендую использовать простой мьютекс/замок, если вы не профилировали его и не обнаружили, что это узкое место.

Ответ 2

Вам повезло, потому что диапазон, который вы хотите, соответствует точно двум битам.

Простое решение: пусть волатильная переменная подсчитывается навсегда. Но после того, как вы его прочитали, используйте только самые младшие два бита (val & 3). Presto, счетчик атомов от 0-3.

Ответ 3

Невозможно создать что-либо атомарное в чистом C, даже с volatile. Вам нужно asm. C1x будет иметь специальные типы атомов, но до тех пор вы застряли в asm.

Ответ 4

У вас две проблемы.

__sync_fetch_and_add вернет значение предыдущее (т.е. перед добавлением одного). Итак, на шаге, где _count становится 3, ваша локальная переменная save_count возвращается 2. Поэтому вам действительно нужно увеличивать _count до 4, пока он не вернется как 3.

Но даже в дополнение к этому, вы специально ищете, чтобы он был >= 4, прежде чем вы вернетесь к нему reset. Это просто вопрос использования неправильного предела, если вы ищете его только для получите до трех.