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

С++ Thread Safe Integer

В настоящее время я создал класс С++ для целочисленного потока, который просто хранит целое число в частном порядке и имеет общедоступные функции набора, которые используют boost:: mutex, чтобы гарантировать, что к целому числу может быть применено только одно изменение за раз.

Это самый эффективный способ сделать это, мне сообщили, что мьютексы достаточно ресурсоемкие? Класс используется много, очень быстро, поэтому он может быть узким местом...

Google С++ Thread Safe Integer возвращает нечеткие представления и рекомендации по безопасности потоков целых операций на разных архитектурах.

Некоторые говорят, что 32-битный int на 32-битной архитектуре безопасен, но 64 на 32 не обусловлен "выравниванием". Другие говорят, что это специфический компилятор/ОС (что я не сомневаюсь).

Я использую Ubuntu 9.10 на 32-битных машинах, некоторые из них имеют два ядра, поэтому в некоторых случаях потоки могут выполняться одновременно на разных ядрах, и я использую компилятор GCC 4.4 g++.

Спасибо заранее...

Обратите внимание: Ответ, который я обозначил как "правильный", наиболее подходит для моей проблемы - однако в других ответах есть несколько отличных точек, и все они заслуживают внимания!

4b9b3361

Ответ 1

Это не компилятор, а конкретная ОС, это специфическая архитектура. Компилятор и ОС входят в него, потому что они - инструменты, с которыми вы работаете, но они не те, которые устанавливают реальные правила. Вот почему стандарт С++ не затронет проблему.

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

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

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

Тем не менее, если вы хотите обрабатывать 64-разрядное целое число как атомное, выровняйте поле с 64-битной границей. Зачем беспокоиться?

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

Предположим, у вас есть N писателей и один читатель. Вы хотите, чтобы авторы могли сигнализировать события читателю. Сами события не имеют данных; вы просто хотите, чтобы количество событий было действительно.

Объявить структуру для разделяемой памяти, совместно используемую всеми писателями и читателем:

#include <stdint.h>
struct FlagTable
{   uint32_t flag[NWriters];
};

(сделайте это класс или шаблон или что-то там, где сочтете нужным.)

Каждому писателю нужно сообщить его индекс и указать указатель на эту таблицу:

class Writer
{public:
    Writer(FlagTable* flags_, size_t index_): flags(flags_), index(index_) {}
    void SignalEvent(uint32_t eventCount = 1);
private:
    FlagTable* flags;
    size_t index;
}

Когда писатель хочет сигнализировать о событии (или нескольких), он обновляет свой флаг:

void Writer::SignalEvent(uint32_t eventCount)
{   // Effectively atomic: only one writer modifies this value, and
    // the state changes when the incremented value is written out.
    flags->flag[index] += eventCount;
}

Читатель хранит локальную копию всех значений флага, которые он видел:

class Reader
{public:
    Reader(FlagTable* flags_): flags(flags_)
    {   for(size_t i = 0; i < NWriters; ++i)
            seenFlags[i] = flags->flag[i];
    }
    bool AnyEvents(void);
    uint32_t CountEvents(int writerIndex);
private:
    FlagTable* flags;
    uint32_t seenFlags[NWriters];
}

Чтобы узнать, произошли ли какие-либо события, он просто ищет измененные значения:

bool Reader::AnyEvents(void)
{   for(size_t i = 0; i < NWriters; ++i)
        if(seenFlags[i] != flags->flag[i])
            return true;
    return false;
}

Если что-то произошло, мы можем проверить каждый источник и подсчитать количество событий:

uint32_t Reader::CountEvents(int writerIndex)
{   // Only read a flag once per function call.  If you read it twice,
    // it may change between reads and then funny stuff happens.
    uint32_t newFlag = flags->flag[i];
    // Our local copy, though, we can mess with all we want since there
    // is only one reader.
    uint32_t oldFlag = seenFlags[i];
    // Next line atomically changes Reader state, marking the events as counted.
    seenFlags[i] = newFlag;
    return newFlag - oldFlag;
}

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

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

Ответ 2

Существует атомная библиотека С++ 0x, а также разрабатывается библиотека Boost.Atomic, в которой используются методы блокировки.

Ответ 3

С++ не имеет реальной реализации целочисленного атома, и большинство распространенных библиотек.

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

Ответ 4

Как вы используете GCC, и в зависимости от того, какие операции вы хотите выполнять над целым, вы можете уйти с GCC atomic builtins.

Они могут быть немного быстрее, чем мьютексы, но в некоторых случаях все еще намного медленнее, чем "обычные" операции.

Ответ 5

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