C++ ориентированное на многопотоковое исполнение целое число

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

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

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

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

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

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

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

17
задан Andrew Grimm 7 October 2010 в 07:05
поделиться

6 ответов

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

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

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

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

Тем не менее, если вы хотите обрабатывать запись 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;
}

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

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

6
ответ дан 30 November 2019 в 13:12
поделиться

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

7
ответ дан 30 November 2019 в 13:12
поделиться

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

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

4
ответ дан 30 November 2019 в 13:12
поделиться

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

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

4
ответ дан 30 November 2019 в 13:12
поделиться

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

2
ответ дан 30 November 2019 в 13:12
поделиться

Вы также можете взглянуть на раздел atomic ops раздела intels Thread Building Blocks или на проект atomic_ops

2
ответ дан 30 November 2019 в 13:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: