Различные способы наблюдения за изменениями данных - PullRequest
4 голосов
/ 02 июля 2010

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

Типичный способ сделать это так:

void MyDataClass::setMember(double d)
{
m_member = d;
notifyAllObservers();
}

Это довольно хороший метод, если член меняется не часто и «классы наблюдения» должны быть в курсе как можно быстрее.

Другой способНаблюдение за изменениями таково:

void MyDataClass::setMember(double d)
{
setDirty();
m_member = d;
}

Это хороший метод, если член изменяется много раз, и «классы наблюдения» регулярно проверяют все «грязные» экземпляры.

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

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

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

Ответы [ 4 ]

6 голосов
/ 02 июля 2010

Два метода, которые вы описали, охватывают (концептуально) оба аспекта, однако я думаю, что вы недостаточно объяснили их плюсы и минусы.

Есть одна вещь, о которой вы должны знать, это фактор населения.

  • Метод push очень хорош, когда много уведомителей и мало наблюдателей
  • Метод Pull отлично подходит, когда мало уведомителей и много наблюдателей

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

Однако есть одна возможность, о которой вы еще не говорили: объединение двух подходов с другим уровнем косвенности.

  • Нажмите каждое изменение на GlobalObserver
  • Пусть каждый наблюдатель проверяет GlobalObserver, когда требуется

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

Epoch 0       Epoch 1      Epoch 2
event1        event2       ...
...           ...

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

Сложность здесь состоит в том, чтобы знать, когда отбрасывать эпохи (когда они больше не нужны). Это требует подсчета ссылок какого-либо рода. Помните, что GlobalObserver - это то, что возвращает текущие эпохи объектам. Поэтому мы вводим счетчик для каждой эпохи, который просто подсчитывает, сколько наблюдателей еще не наблюдали эту эпоху (и последующие).

  • При подписке мы возвращаем номер эпохи и увеличиваем счетчик этой эпохи
  • При опросе мы уменьшаем счетчик опрошенной эпохи, возвращаем текущий номер эпохи и увеличиваем его счетчик
  • При отписке мы уменьшаем счетчик эпохи -> следим, чтобы деструктор отписался!

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

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

4 голосов
/ 02 июля 2010

другие приемы наблюдения за изменениями данных

Не совсем. У вас есть «толкать» и «тянуть» шаблоны проектирования. Других вариантов нет.

A notifyAllObservers - это push , доступ к обычным атрибутам - pull .

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

Не смущайтесь.

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

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

class PeriodicObserver {
    bool dirty;
    public void notification(...) {
        // save the changed value; do nothing more.  Speed matters.
        this.dirty= True;
    }
    public result getMyValue() {
        if( this.dirty ) { 
            // recompute now
        }
        return the value
}
2 голосов
/ 02 июля 2010

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

class notifier { 
public:
    virtual void operator()() = 0;
};

class pull_notifier : public notifier { 
    bool dirty;
public:
    lazy_notifier() : dirty(false) {}
    void operator()() { dirty = true; }
    operator bool() { return dirty; }
};

class push_notifier : public notifier { 
    void (*callback)();
public:
    push_notifier(void (*c)()) : callback(c) {}
    void operator()() { callback(); }
};

Тогда наблюдатель может передать либо push_notifier, либо pull_notifier, как сочтет нужным, и мутатору не нужно заботиться о разнице:

class MyDataClass { 
    notifier &notify;
    double m_member;
public:
    MyDataClass(notifier &n) : n_(n) {}
    void SetMember(double d) { 
        m_member = d; 
        notify();
    }
};

На данный момент я написал это только с одним наблюдателем на мутатора, но довольно просто изменить его на вектор указателей на объекты-наблюдатели для данного мутатора, если вам нужно больше. При этом данный мутатор будет поддерживать произвольную комбинацию уведомлений push_ и pull_. Если вы уверены, что данный мутатор будет когда-либо использовать только pull_notifier s или push_notifiers, вы можете рассмотреть возможность использования шаблона с уведомителем в качестве параметра шаблона (политики), чтобы избежать издержек при вызове виртуальной функции (вероятно, незначительным для push_notifier, но менее для pull_notifier).

0 голосов
/ 02 июля 2010

Вы описали две доступные опции высокого уровня (push vs pull / polling).Других вариантов, о которых я знаю, нет.

...