[Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 17.03.09 09:18
Оценка: 189 (17)
Тема избитая, однако попробуем поднять планку ещё выше, т.е. ещё меньше "синтаксического шума" для пользователя.
Задача, что бы для пользователя это выглядело следующим образом:


// описываем класс сообщения
struct my_msg : msg<my_msg>
{
    int data;
    my_msg(int data) : data(data) {}
};

// описываем класс получателя
class my_agent : public agent<my_agent>
{
public:
    my_agent(int data) : data(data) {}
    void on_event(event<my_msg> ev)
    {
        std::cout << "on_event: my_agent::data=" << data << ", my_msg::data=" << ev.msg->data << std::endl;
    }
private:
    int data;
};

// собственно всё, дальше можем отправлять полиморфные сообщения полиморфному получателю
// т.е. ни тип сообщения, ни тип получателя при отправке уже не нужны
int main()
{
    my_agent d (57);
    my_msg m (113);

    agent_base* b = &d;
    msg_base* mb = &m;
    b->send(*mb);
}


Всё, что требуется для регистрации обработчика, — это отнаследовать получателя от CRTP базы и объявить метод обработчика, который принимает специальный объект event<msg_t> (этот event помимо самого сообщения может так же содержать отправителя сообщения и другую контекстную информацию).

Ниже приводится реализация. Большая часть особого интереса не представляет, т.к. занимается раздачей уникальных идентификаторов типам сообщений и типам получателей. Наибольший интерес представляет класс agent<>::event<>, который как раз и регистрирует обработчик с помощью "конструктора класса" (http://www.rsdn.ru/forum/message/3089286.1.aspx
Автор: remark
Дата: 03.09.08
). Суть приёма — метод класса регистрируется за счёт "упоминания" класса event<msg>; при этом класс получателя определяется автоматически за счёт того, что event<> не глобальный класс, а определен в CRTP базе (в противном случае пришлось бы писать void on_event(event<my_agent, my_msg> ev)).

std::map<int, std::map<int, void(*)(void*, void const*)> > handlers;

struct msg_base
{
    virtual int get_id() const = 0;

protected:
    static int id_seq_;
};

struct agent_base
{
    virtual int get_id() const = 0;

    void send(msg_base const& m)
    {
        handlers[get_id()][m.get_id()](this, &m);
    }

protected:
    static int id_seq_;
};

template<typename derived_t>
class agent : public agent_base
{
protected:
    template<typename msg_t>
    struct event : class_initializer<event<msg_t> >
    {
        msg_t const* msg;
        agent_base* sender;

    private:
        friend class class_initializer<event<msg_t> >;
        static void static_ctor()
        {
            handlers[derived_t::static_id()][msg_t::static_id()] = &agent::thunk<msg_t>;
        }
    };

    template<typename msg_t>
    static void thunk(void* d, void const* m)
    {
        event<msg_t> ev;
        ev.msg = static_cast<msg_t const*>(static_cast<msg_base const*>(m));
        ev.sender = 0; // просто для примера
        static_cast<derived_t*>(static_cast<agent_base*>(d))->on_event(ev);
    }

    static int id_;

    virtual int get_id() const
    {
        return static_id();
    }

public:
    static int static_id()
    {
        if (id_ == 0)
            id_ = ++agent_base::id_seq_;
        return id_;
    }
};

template<typename derived_t>
class msg : public msg_base
{
private:
    static int id_;

    virtual int get_id() const
    {
        return static_id();
    }

public:
    static int static_id()
    {
        if (id_ == 0)
            id_ = ++msg_base::id_seq_;
        return id_;
    }
};

// определения статических членов (в порядке появления):
int msg_base::id_seq_;
int agent_base::id_seq_;
template<typename derived_t> int agent<derived_t>::id_;
template<typename derived_t> int msg<derived_t>::id_;


Если требуется наследование классов получателей/сообщений, то это решение необходимо скрестить с CRTP v3.0 (http://www.rsdn.ru/forum/message/2677311.1.aspx
Автор: remark
Дата: 02.10.07
).

Единственная засада, которая пока видится, — если класс получателя шаблонный, то он не сможет называть event<T> просто как event<T>, т.к. он будет сидеть в шаблонной базе (не актуально для MSVC — она это позволяет). Решить можно добавлением "using agent<my_agent<T> >::event" в объявление шаблонного получателя.


1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re: [Trick] Диспетчеризация сообщений
От: eao197 Беларусь http://eao197.blogspot.com
Дата: 17.03.09 09:41
Оценка:
Здравствуйте, remark, Вы писали:

R>
R>    template<typename msg_t>
R>    static void thunk(void* d, void const* m)
R>    {
R>        event<msg_t> ev;
R>        ev.msg = static_cast<msg_t const*>(static_cast<msg_base const*>(m));
R>        ev.sender = 0; // просто для примера
R>        static_cast<derived_t*>(static_cast<agent_base*>(d))->on_event(ev);
R>    }
R>


Правильно ли я понимаю, что для какого-то конкретного типа сообщения M в классе может быть только один его обработчик с именем on_event?


SObjectizer: <микро>Агентно-ориентированное программирование на C++.
Re: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 17.03.09 10:24
Оценка:
Здравствуйте, remark, Вы писали:

R> ...


Супер, только я бы ещё от виртуальности избавился, скопировав id в конструкторе базы.
Re[2]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 17.03.09 11:48
Оценка:
Здравствуйте, sokel, Вы писали:

S>Супер, только я бы ещё от виртуальности избавился, скопировав id в конструкторе базы.


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


1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re[2]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 17.03.09 11:57
Оценка:
Здравствуйте, eao197, Вы писали:

E>Здравствуйте, remark, Вы писали:


R>>
R>>    template<typename msg_t>
R>>    static void thunk(void* d, void const* m)
R>>    {
R>>        event<msg_t> ev;
R>>        ev.msg = static_cast<msg_t const*>(static_cast<msg_base const*>(m));
R>>        ev.sender = 0; // просто для примера
R>>        static_cast<derived_t*>(static_cast<agent_base*>(d))->on_event(ev);
R>>    }
R>>


E>Правильно ли я понимаю, что для какого-то конкретного типа сообщения M в классе может быть только один его обработчик с именем on_event?


Непосредственно в этой реализации — да.
Но если допустим в какой-то гипотетической библиотеке надо так же иметь возможность иметь несколько обработчиков для одного сообщения в разных состояниях, то можно модифицировать эту схему.
Вариант первый будет выглядеть как:
class my_agent : agent<my_agent>
{
  void on_event(event<my_msg, state_start> ev)
  {
  }
  void on_event(event<my_msg, state_error> ev)
  {
  }
};


Вариант второй будет выглядеть как:
class my_agent : agent<my_agent>
{
  MSG_MAP(
    (state_init,
      (my_msg1, on_request)
      (my_msg2, on_error)
    )
    (state_error,
      (my_msg1, on_error)
      (my_msg2, on_error)
    )
  );

  void on_request(event<my_msg1> ev)
  {
  }
  void on_error(event<my_msg1> ev)
  {
  }
  void on_error(event<my_msg2> ev)
  {
  }
};


В принципе тут можно наследовать класс агента от некого специального state_based_agent<derived_t>, что бы отличать таких агентов от простых агентов без состояний. А можно и не наследовать, т.к. я думаю возможно это отличать автоматически.



1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re[3]: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 17.03.09 12:24
Оценка:
Здравствуйте, remark, Вы писали:

R>Я думаю, с этим никаких проблем не будет.

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

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

R>... а можно и захачить таблицу виртуальных функций и вместо указателя на функцию get_id() класть туда непосредственно сам идентификатор

Хех, такой изврат мне в голову ни разу не приходил... Это уже конечно не виртуальный вызов, но косвенная адресация всё равно останется.
Re[4]: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 17.03.09 14:14
Оценка:
S>Здравствуйте, remark, Вы писали:

R>>Я думаю, с этим никаких проблем не будет.

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

С виртуальностью, кстати, можно не общую таблицу сделать, а отдельные статические для каждого агента. И заменить, соответственно, agent_base::get_id() на agent_base::get_handlers(). А уж если позволить себе handlers[MAX_AGENT_ID][MAX_MSG_ID], то вообще летать будет.
Re[4]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 17.03.09 17:29
Оценка:
Здравствуйте, sokel, Вы писали:

R>>Я думаю, с этим никаких проблем не будет.

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

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


Базовый механизм этого не навязывает. Это полностью на усмотрение пользователя.


R>>... а можно и захачить таблицу виртуальных функций и вместо указателя на функцию get_id() класть туда непосредственно сам идентификатор

S>Хех, такой изврат мне в голову ни разу не приходил... Это уже конечно не виртуальный вызов, но косвенная адресация всё равно останется.

Зато дополнительная память в объекте не расходуется. Лучшего варианта тут просто нет.



1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re[5]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 17.03.09 19:11
Оценка:
Здравствуйте, sokel, Вы писали:

S>>Здравствуйте, remark, Вы писали:


R>>>Я думаю, с этим никаких проблем не будет.

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

S>С виртуальностью, кстати, можно не общую таблицу сделать, а отдельные статические для каждого агента. И заменить, соответственно, agent_base::get_id() на agent_base::get_handlers(). А уж если позволить себе handlers[MAX_AGENT_ID][MAX_MSG_ID], то вообще летать будет.


Сложный вопрос... честно говоря, я не знаю ответа. Увеличенное потрбеление памяти (потенциально мегабайты) тоже рано или поздно скажется на производительности, занимать значительную часть кэша процессора этой таблицей тоже не хочется...
Я думал о чём-то типа такого: пересортировать идентификаторы получателей и сообщений, т.ч. у каждого получателя идентификаторы сообщений будут как можно ближе друг к другу (гипотеза, что это можно сделать достаточно хорошо). Далее для каждого получателя можно хранить не всю строчку из MAX_MSG_ID элементов, а лишь небольшое подмножество MINi..MAXi. Проще показать в коде:
struct receiver_desc_t
{
  int min_id;
  int max_id;
  handler_t* array;

  handler_t* get_handler(int msg_id)
  {
    if (msg_id >= min_id && msg_id < max_id)
      return array[msg_id - min_id];
    else
      return 0;     
  }
};





1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re[6]: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 18.03.09 13:08
Оценка: 17 (1)
Здравствуйте, remark, Вы писали:

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

R>Я думал о чём-то типа такого: пересортировать идентификаторы получателей и сообщений, т.ч. у каждого получателя идентификаторы сообщений будут как можно ближе друг к другу (гипотеза, что это можно сделать достаточно хорошо). Далее для каждого получателя можно хранить не всю строчку из MAX_MSG_ID элементов, а лишь небольшое подмножество MINi..MAXi.

Я делал что-то подобное в виде обёртки мапа с целочисленными ключами. По заданному ограничению при возможности складывал ссылки на итераторы в массив. А переопределенный find либо брал итератор из массива либо перенаправлял в find map'а. Можно усложнить — сделать реализацию, которая будет даже при превышении ограничения выполнять частичный перевод дерева в табличный вид, типа искать максимальный по количеству диапазон ключей, не превышающий ограничение по разнице min-max, а затем разбивать map на три части — map до min_id, массив, map после max_id.
Re: [Trick] Диспетчеризация сообщений
От: SenkraD Украина  
Дата: 18.03.09 18:45
Оценка:
Здравствуйте, remark, Вы писали:
R>template<typename derived_t>
R>class agent : public agent_base
R>{
R>protected:
R> template<typename msg_t>
R> struct event : class_initializer<event<msg_t> >
R> {
R> msg_t const* msg;
R> agent_base* sender;

R> private:

R> friend class class_initializer<event<msg_t> >; /* У меня это не прошло — это глюс компилера или я таки правильно понимаю как работают "друзья" */
R> static void static_ctor()
R> {
R> handlers[derived_t::static_id()][msg_t::static_id()] = &agent::thunk<msg_t>;/* и на это компилер тоже матерится — не может понять как взять адрес такой функции. */
R> }
R> };

R> template<typename msg_t>

R> static void thunk(void* d, void const* m)
R> {
R> event<msg_t> ev;
R> ev.msg = static_cast<msg_t const*>(static_cast<msg_base const*>(m));
R> ev.sender = 0; // просто для примера
R> static_cast<derived_t*>(static_cast<agent_base*>(d))->on_event(ev);
R> }

R> static int id_;


R> virtual int get_id() const

R> {
R> return static_id();
R> }

R>public:

R> static int static_id()
R> {
R> if (id_ == 0)
R> id_ = ++agent_base::id_seq_;
R> return id_;
R> }
R>};

Машина: Mac PPC, tiger
Компилер: gcc 4.0.2
Re[7]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 19.03.09 21:12
Оценка:
Здравствуйте, sokel, Вы писали:

S>Я делал что-то подобное в виде обёртки мапа с целочисленными ключами. По заданному ограничению при возможности складывал ссылки на итераторы в массив. А переопределенный find либо брал итератор из массива либо перенаправлял в find map'а. Можно усложнить — сделать реализацию, которая будет даже при превышении ограничения выполнять частичный перевод дерева в табличный вид, типа искать максимальный по количеству диапазон ключей, не превышающий ограничение по разнице min-max, а затем разбивать map на три части — map до min_id, массив, map после max_id.



Неплохой вариант. Напоминает реализацию таблиц в Lua, они там тоже некоторый плотный поддиапазон опционально хранят в массиве, а оставшуюся часть (если есть) в дереве.
Если не жалко времени на реализацию, то такой вариант наверное один из самых лучших, что я видел.
Я думаю, что оставшуюся часть уже можно хранить и в одном дереве (не обязательно бить на 2 части), практически ничего не изменится.



1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re[2]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 19.03.09 21:13
Оценка:
Здравствуйте, SenkraD, Вы писали:

R>> friend class class_initializer<event<msg_t> >; /* У меня это не прошло — это глюс компилера или я таки правильно понимаю как работают "друзья" */

R>> handlers[derived_t::static_id()][msg_t::static_id()] = &agent::thunk<msg_t>;/* и на это компилер тоже матерится — не может понять как взять адрес такой функции. */
SD>Машина: Mac PPC, tiger
SD>Компилер: gcc 4.0.2


Я набросал только под MSVC9, если надо под другие компиляторы, то надо допиливать напильником.



1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
Re: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 25.08.09 08:31
Оценка: 14 (1)
Здравствуйте, remark, Вы писали:

R>// собственно всё, дальше можем отправлять полиморфные сообщения полиморфному получателю

R>// т.е. ни тип сообщения, ни тип получателя при отправке уже не нужны

Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:

// базовый тип сообщения
class msg
{
    template<typename> friend class msgqueue;
    unsigned int id;
};
// сочередь сообщений
template<typename impl_type>
class msgqueue
{
public:
    template<typename msg_type>
    void post(msg_type* m)
    {
        msg* b = static_cast<msg*>(m);
        b->id = handler<msg_type>::id;
        // здесь помещаем в очередь, потом эту очередь откуда то разгребаем
        // но это пример, так что сразу синхронно вызовем dispatch
        dispatch_message(b);    
    }
private:
    void post(msg*); // закрываем возможность отправки сообщений через базовый тип
    // таблица обработчиков сообщений, генерируется при инициализации статики
    static std::vector<void(*)(void*, void*)> handlers;
    // собственно handler
    template<typename msg_type>
    struct handler : static_initializer<handler<msg_type> >
    {
        static unsigned int id;
        // регистрируем обработчик в таблице
        static void static_ctor()
        {
            id = (unsigned int) handlers.size();
            handlers.push_back(handle);            
        }
        // функция обработки сообщения
        static void handle(void* q, void* m) 
        {
            static_cast<impl_type*>(q)->on_msg(static_cast<msg_type*>(m));
            delete static_cast<msg_type*>(m); // и виртуальный деструктор не требуется
        }
    };
    // метод сопоставления сообщению обработчика
    void dispatch_message(msg* m)
    {
        handlers[m->id](this, m);
    }
};
// инициализируем статику
template<typename impl_type> std::vector<void(*)(void*, void*)> msgqueue<impl_type>::handlers;
template<typename impl_type> template<typename msg_type> unsigned int msgqueue<impl_type>::handler<msg_type>::id = 0;


ну и пример использования:
struct my_msg : msg
{
    my_msg(int i) : i(i) {}
    int i;
};
struct my_queue : msgqueue<my_queue>
{
    void on_msg(my_msg* m)
    {
        printf("%d\n", m->i);
    }
};

int main() 
{
    my_queue msgq;
    // такая запись заставляет инстанцировать метод my_queue::on_message(my_msg*)
    msgq.post(new my_msg(42)); 
    // особенность подхода: нельзя делать post через базу
    // msgq.post(static_cast<msg*>(new my_msg(42))); // ошибка - private метод
}
Re[2]: [Trick] Диспетчеризация сообщений
От: sokel Россия  
Дата: 25.08.09 08:51
Оценка:
Здравствуйте, sokel, Вы писали:

S>Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:


хотя, в этом случае, можно вообще обойтись и без таблицы и без статики:
// базовый тип сообщения
class msg
{
    template<typename> friend class msgqueue;
    void (* handle)(void*, void*);
    unsigned int id;
};
// сочередь сообщений
template<typename impl_type>
class msgqueue
{
public:
    template<typename msg_type>
    void post(msg_type* m)
    {
        m->handle = handle<msg_type>;
        // здесь помещаем в очередь, потом эту очередь откуда то разгребаем
        // но это пример, так что сразу вызовем dispatch
        dispatch_message(m);    
    }
private:
    void post(msg*); // закрываем возможность отправки сообщений через базовый тип
    // функция обработки сообщения
    template<typename msg_type>
    static void handle(void* q, void* m) 
    {
        static_cast<impl_type*>(q)->on_msg(static_cast<msg_type*>(m));
        delete static_cast<msg_type*>(m); // и виртуальный деструктор не требуется
    }
    // метод обработки сообщения
    void dispatch_message(msg* m) { m->handle(this, m); }
};
Re[3]: [Trick] Диспетчеризация сообщений
От: remark Россия http://www.1024cores.net/
Дата: 25.08.09 09:16
Оценка:
Здравствуйте, sokel, Вы писали:

S>Здравствуйте, sokel, Вы писали:


S>>Можно я тему подниму? Бывает ведь и такой случай: тип сообщения и тип получателя на момент отправки известны, например, какая нибудь очередь асинхронной обработки сообщений. В этом случае можно всё сделать ещё проще: регистрировать обработчик в шаблонном методе отправки сообщений. А идентификатор сообщения инициализировать непосредственно в момент отправки:


S>хотя, в этом случае, можно вообще обойтись и без таблицы и без статики:


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


1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.