Это практически безопасно для записи статических данных из нескольких потоков - PullRequest
1 голос
/ 26 февраля 2009

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

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

Мой класс обработки данных будет выглядеть примерно так:

class StatusCache
{
public:
    static void SetActivityStarted(bool activityStarted)
        { m_activityStarted = activityStarted; WriteToDB(); }
    static void SetActivityComplete(bool activityComplete);
        { m_activityComplete = activityComplete; WriteToDB(); }
    static void SetProcessReady(bool processReady);
        { m_processReady = processReady; WriteToDB(); }
    static void SetProcessPending(bool processPending);
        { m_processPending = processPending; WriteToDB(); }
private:
    static void WriteToDB(); // will write all the class data to the db (multiple requests will happen in series)
    static bool m_activityStarted;
    static bool m_activityComplete;
    static bool m_processReady;
    static bool m_processPending;
};

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

Неважно, если в обновлении базы данных есть некоторое перекрытие между двумя потоками, например,

thread 1                        thread 2                    activity started in db
SetActivityStarted(true)        SetActivityStarted(false)   
  m_activityStated = true
                                  m_activityStarted = false
                                  WriteToDB()               false
  WriteToDB()                                               false

Таким образом, БД показывает статус, который был недавно установлен линиями m _... = x. Это нормально.

Это разумный подход к использованию или есть лучший способ сделать это?

[Отредактировано, чтобы заявить, что я забочусь только о последнем статусе - порядок не важен]

Ответы [ 10 ]

8 голосов
/ 26 февраля 2009

Нет, это не безопасно.

Сгенерированный код, который выполняет запись в m_activityStarted и другие, может быть атомарным, но это не гарантируется. Кроме того, в ваших установщиках вы делаете две вещи: устанавливаете логическое значение и делаете вызов. Это определенно не атомарно.

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

Например, один поток может вызвать первую функцию, и, прежде чем этот поток перейдет в «WriteDB ()», другой поток может вызвать другую функцию и перейти в WriteDB () без первого перехода туда. Тогда, возможно, статус записывается в БД в неправильном порядке.

Если вы беспокоитесь о взаимоблокировках, вам следует пересмотреть всю стратегию параллелизма.

3 голосов
/ 26 февраля 2009

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

Если вы не заглушите это, в конце концов вы об этом пожалеете!

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

Мьютексинг тривиален.

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

Вы будете в порядке, если будете следовать золотым правилам:

  • Не смешивайте команды блокировки мьютекса. Например. A.lock (); B.lock () в одном месте и B.lock (); A.lock (); в другой. Используйте один заказ или другой!
  • Блокировка на максимально короткое время.
  • Не пытайтесь использовать один мьютекс для нескольких целей. Используйте несколько мьютексов.
  • По возможности используйте рекурсивные или проверочные мьютексы.
  • Используйте RAII или макросы для обеспечения разблокировки.

например:.

#define RUN_UNDER_MUTEX_LOCK( MUTEX, STATEMENTS ) \
   do { (MUTEX).lock();  STATEMENTS;  (MUTEX).unlock(); } while ( false )

class StatusCache
{
public:
    static void SetActivityStarted(bool activityStarted)
        { RUN_UNDER_MUTEX_LOCK( mMutex, mActivityStarted = activityStarted );
          WriteToDB(); }
    static void SetActivityComplete(bool activityComplete);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mActivityComplete = activityComplete );
          WriteToDB(); }
    static void SetProcessReady(bool processReady);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mProcessReady = processReady );
          WriteToDB(); }
    static void SetProcessPending(bool processPending);
        { RUN_UNDER_MUTEX_LOCK( mMutex, mProcessPending = processPending );
          WriteToDB(); }

private:
    static void WriteToDB(); // read data under mMutex.lock()!

    static Mutex mMutex;
    static bool  mActivityStarted;
    static bool  mActivityComplete;
    static bool  mProcessReady;
    static bool  mProcessPending;
};
3 голосов
/ 26 февраля 2009

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

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

1 голос
/ 26 февраля 2009

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

1 голос
/ 26 февраля 2009

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

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

А в C ++ целые гарантированно занимают по крайней мере один байт. Это означает, что на самом деле они могут иметь значение, отличное от true или false, скажем, из-за состояния гонки, когда чтение происходит в процессе записи.

В .Net имеется некоторая автоматическая синхронизация статических данных и статических методов. В стандарте C ++ такой гарантии нет.

Если вы смотрите только на int, bools и (я думаю) long, у вас есть несколько вариантов атомарного чтения / записи и сложения / вычитания. C ++ 0x имеет что-то . Как и Intel TBB . Я считаю, что большинство операционных систем также имеют необходимые крючки для достижения этой цели.

1 голос
/ 26 февраля 2009

Похоже, у вас есть две проблемы здесь.

# 1 заключается в том, что ваше логическое назначение не обязательно является атомарным, даже если это один вызов в вашем коде. Так что под капотом у вас может возникнуть противоречивое состояние. Вы можете использовать atomic_set (), если ваша библиотека потоков / параллелизма поддерживает это.

# 2 - синхронизация между чтением и письмом. Из вашего примера кода, похоже, что ваша функция WriteToDB () записывает состояние всех 4 переменных. Где сериализуется WriteToDB? Может возникнуть ситуация, когда thread1 запускает WriteToDB (), которая читает m_activityStarted, но не заканчивает запись его в базу данных, а затем вытесняется thread2, который записывает m_activityStarted на всем протяжении. Затем thread1 возобновляет работу и заканчивает запись своего несовместимого состояния в базу данных. По крайней мере, я думаю, что у вас должен быть доступ на запись к статическим переменным, заблокированным, пока вы выполняете права на чтение, необходимые для обновления базы данных.

1 голос
/ 26 февраля 2009

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

0 голосов
/ 26 февраля 2009

Как уже отмечали другие, это вообще очень плохая идея (с некоторыми оговорками).

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

Предостережение: если все ваши сеттеры делают атомарные записи, и если вы не заботитесь о сроках их выполнения, тогда вы можете быть в порядке.

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

0 голосов
/ 26 февраля 2009

Это не очень хорошая идея. Есть много переменных, которые будут влиять на синхронизацию различных потоков.

Без какой-либо блокировки вам не будет гарантировано правильное последнее состояние.

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

Пока код блокировки спроектирован правильно, мертвые блокировки не должны быть проблемой с таким простым процессом.

0 голосов
/ 26 февраля 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...