C ++ Thread Safe Integer - PullRequest
       3

C ++ Thread Safe Integer

17 голосов
/ 28 апреля 2010

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

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

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

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

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

Заранее спасибо ...

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

Ответы [ 6 ]

7 голосов
/ 28 апреля 2010

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

6 голосов
/ 28 апреля 2010

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

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

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

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

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

РЕДАКТИРОВАТЬ: Для случая, который вы упомянули в своем комментарии, мне понадобится больше информации, чтобы быть уверенным, поэтому позвольте мне привести пример того, что может быть реализовано без специального кода синхронизации.

Предположим, у вас есть 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;
}

А теперь, во всем этом большая ошибка? Это неблокирование, то есть вы не можете заставить Читатель спать, пока Писатель что-то не напишет. Считыватель должен выбрать между сидением в спин-петле и ожиданием возврата AnyEvents(), чтобы вернуть true, что минимизирует задержку, или он может каждый раз немного спать, что экономит процессор, но может позволить нарастить много событий. Так что лучше, чем ничего, но это не решение для всего.

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

4 голосов
/ 28 апреля 2010

Поскольку вы используете GCC, и в зависимости от того, какие операции вы хотите выполнить с целым числом, вам может не понравиться атомарные встроенные функции GCC .

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

4 голосов
/ 28 апреля 2010

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

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

2 голосов
/ 28 апреля 2010

Вы также можете взглянуть на секцию atomic ops в Intels Thread Building Blocks или atomic_ops project

2 голосов
/ 28 апреля 2010

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

...