Синхронизация для обсервера
От: s_viy  
Дата: 18.09.07 08:53
Оценка:
Приветствую! Собственно интересует общая стратегия синхронизация двух
связных объектов. Вот пример псевдо-кода для обсервера:

struct iobserver
{
  virtual void update() = 0;
  virtual void release() = 0;
};

struct isubject
{
  virtual void method() = 0;
  virtual void erase(iobserver*) = 0;
};

struct subject: isubject
{
  mutex _m;
  iobserver* _observer;
  virtual void method()
  {
    mutex::scoped_lock sl(_m);
    _observer->update();
  }
  virtual void erase(iobserver* observer)
  {
    mutex::scoped_lock sl(_m);
    _observer = 0;
  }
};

struct observer: iobserver
{
  mutex _m;
  isubject* _subject;
  virtual void update()
  {
    mutex::scoped_lock sl(_m);
    this->release();
  };

  virtual void release()
  {
    mutex::scoped_lock sl(_m);
    _subject->erase(this);
  };
};


Добустим из одного потока вызывается method субъекта, который
вызывает метод update обсервера, в результате его выполнения
обсервер решает, что больше он не нужен и вызывает метод release
где призводит все необходимые операции и отписывается от субъекта.
В это время из другого потока, также происходит вызов метода
release() обсервера. Если этот вызов происходит после вызова
update(), но до вызова release() из update(), то на методе
erase() возникает деадлок. Интересует, как эту проблему можно
элегантно решить?
Re: Синхронизация для обсервера
От: Аноним  
Дата: 18.09.07 11:27
Оценка:
struct iobserver
{
  virtual void update() = 0;
  virtual void release() = 0;
};

struct observer: iobserver
{
  mutex _m;
  virtual void update()
  {
    mutex::scoped_lock sl(_m);
    this->release();
  };

  virtual void release()
  {
    mutex::scoped_lock sl(_m);
  };
};


Помоему просто вызов update — это уже deadlock или я чтото не понимаю?
Re: Синхронизация для обсервера
От: Кодт Россия  
Дата: 18.09.07 11:35
Оценка:
Здравствуйте, s_viy, Вы писали:

_>Приветствую! Собственно интересует общая стратегия синхронизация двух связных объектов.

_>struct subject: isubject
_>{
_>  mutex _m;
_>  ...
_>};

_>struct observer: iobserver
_>{
_>  mutex _m;
_>  ...
_>};

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

Выходов 4:

  • Алгоритм банкира и другие способы глобально следить за порядком захвата мьютексов.

  • Отказаться от локальных мьютексов и сделать апартаменты (самая примитивная реализация — просто глобальный мьютекс).
    Поток заходит в апартаменты и делает там всё, что хочет; в это время другой поток ждёт у дверей.
    Подробности такой модели — читай в книжках по COM, там эта техника используется вовсю.

  • Сделать "локальные апартаменты" у каждого объекта. Выход за границу объекта (вызов стороннего метода) приводит к выходу из апартаментов.
    class subject : public isubject
    {
        mutex m_;
        iobserver* observer_;
        
        void method()
        {
            mutex::scoped_lock lk(m_);
            // в апартаментах
            .....
            // здесь объект находится в согласованном состоянии,
            // и мы безопасно выходим...
            lk.unlock();
            observer_->update();
            lk.lock();
            // снова в апартаментах
            .....
            // вышли из блока - вышли из апартаментов
        }
    };
    // Аналогично - observer

    Эта техника хорошо известна при работе с conditional variable — только там сама cv обеспечивает выход-вход.

  • Отказаться от синхронных вызовов, заменив их на посылку асинхронных сообщений.
    Пусть observer живёт в своём потоке, а subject'ы посылают туда сигналы обновиться и отписаться. (Естественно, отписка более приоритетна, чем обновление и даже, возможно будет синхронной).
    Способ посылки сигнала — в ассортименте. Начиная с атомарного булева флажка и кончая очередью.
    ... << RSDN@Home 1.2.0 alpha rev. 655>>
  • Перекуём баги на фичи!
    Re[2]: Синхронизация для обсервера
    От: s_viy  
    Дата: 18.09.07 11:59
    Оценка:
    Здравствуйте, Аноним, Вы писали:

    А>Помоему просто вызов update — это уже deadlock или я чтото не понимаю?


    Если mutex рекурсивный то нет, но это уже другая проблема, не менее интересная
    Re[2]: Синхронизация для обсервера
    От: s_viy  
    Дата: 18.09.07 12:23
    Оценка:
    Здравствуйте, Кодт, Вы писали:

    К>
  • Алгоритм банкира и другие способы глобально следить за порядком захвата мьютексов.
    Что-то он мне показался чересчур ресурсоемким, но еще не смотрел подробно

    К>
  • Отказаться от локальных мьютексов и сделать апартаменты (самая примитивная реализация — просто глобальный мьютекс).
    К>Поток заходит в апартаменты и делает там всё, что хочет; в это время другой поток ждёт у дверей.
    К>Подробности такой модели — читай в книжках по COM, там эта техника используется вовсю.
    Насколько я помню COM один объект может принадлежать только одному апартаменту, т.е. для subject и всех его observer,
    должен быть один мутекс. Но если observer наблюдает еще за другим объектом, то его тоже надо в ключать в этот апартамент и т.д.
    что не хотелось бы, т.к. обсерверы разнесены по потокам т.к. в моем проекте достаточно ресурсоемки

    К>
  • Сделать "локальные апартаменты" у каждого объекта. Выход за границу объекта (вызов стороннего метода) приводит к выходу из апартаментов.
    К>
    К>class subject : public isubject
    К>{
    К>    mutex m_;
    К>    iobserver* observer_;
        
    К>    void method()
    К>    {
    К>        mutex::scoped_lock lk(m_);
    К>        // в апартаментах
    К>        .....
    К>        // здесь объект находится в согласованном состоянии,
    К>        // и мы безопасно выходим...
    К>        lk.unlock();
    К>        observer_->update();
    К>        lk.lock();
    К>        // снова в апартаментах
    К>        .....
    К>        // вышли из блока - вышли из апартаментов
    К>    }
    К>};
    К>// Аналогично - observer
    К>

    К>Эта техника хорошо известна при работе с conditional variable — только там сама cv обеспечивает выход-вход.
    Такой подход применим для локальных вызовов при нерекурсиавных мутексах. В данном примере, обсервер
    может отписаться из другого потока после lk.unlock(); и до observer_->update();, что приведет к ошибке.

    К>
  • Отказаться от синхронных вызовов, заменив их на посылку асинхронных сообщений.
    К>Пусть observer живёт в своём потоке, а subject'ы посылают туда сигналы обновиться и отписаться. (Естественно, отписка более приоритетна, чем обновление и даже, возможно будет синхронной).
    К>Способ посылки сигнала — в ассортименте. Начиная с атомарного булева флажка и кончая очередью.
    В принципе у меня так и сделанно (через очередь), но мне не нравиться что много кода работы с очередью, хотя классы по сути простые.

    В общем большое спасибо, Кодт, за ответ, подумаю над апартаментами или оставлю последний вариант, жаль что простого решения нет.
  • Re[3]: Синхронизация для обсервера
    От: Кодт Россия  
    Дата: 18.09.07 12:53
    Оценка:
    Здравствуйте, s_viy, Вы писали:

    К>>
  • Алгоритм банкира и другие способы глобально следить за порядком захвата мьютексов.
    _>Что-то он мне показался чересчур ресурсоемким, но еще не смотрел подробно
    Естественно. Я его упомянул так, для общего развития.

    К>>
  • Отказаться от локальных мьютексов и сделать апартаменты (самая примитивная реализация — просто глобальный мьютекс).
    К>>Поток заходит в апартаменты и делает там всё, что хочет; в это время другой поток ждёт у дверей.
    К>>Подробности такой модели — читай в книжках по COM, там эта техника используется вовсю.
    _>Насколько я помню COM один объект может принадлежать только одному апартаменту, т.е. для subject и всех его observer,
    _>должен быть один мутекс. Но если observer наблюдает еще за другим объектом, то его тоже надо в ключать в этот апартамент и т.д.
    _>что не хотелось бы, т.к. обсерверы разнесены по потокам т.к. в моем проекте достаточно ресурсоемки

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

    К>>
  • Сделать "локальные апартаменты" у каждого объекта. Выход за границу объекта (вызов стороннего метода) приводит к выходу из апартаментов.
    К>>Эта техника хорошо известна при работе с conditional variable — только там сама cv обеспечивает выход-вход.
    _>Такой подход применим для локальных вызовов при нерекурсиавных мутексах. В данном примере, обсервер
    _>может отписаться из другого потока после lk.unlock(); и до observer_->update();, что приведет к ошибке.

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

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

    К>>
  • Отказаться от синхронных вызовов, заменив их на посылку асинхронных сообщений.
    К>>Пусть observer живёт в своём потоке, а subject'ы посылают туда сигналы обновиться и отписаться. (Естественно, отписка более приоритетна, чем обновление и даже, возможно будет синхронной).
    К>>Способ посылки сигнала — в ассортименте. Начиная с атомарного булева флажка и кончая очередью.
    _>В принципе у меня так и сделанно (через очередь), но мне не нравиться что много кода работы с очередью, хотя классы по сути простые.

    _>В общем большое спасибо, Кодт, за ответ, подумаю над апартаментами или оставлю последний вариант, жаль что простого решения нет.


    Дык, это архитектурная проблема.
    А что касается работы с очередью — попробуй отделить механику очередей от бизнес-логики.

    Ещё один вариант — переделать всё на conditional variable.
    struct subject
    {
        .....
        mutex m_;
        conditional_variable cv_;
        .....
        iobserver* observer_;
        bool not_used_;
        bool not_used() const { return not_used_; }
        .....
        
        void erase()
        {
            scoped_lock lk(m_);
            cv_.wait(lk, bind(&subject::not_used,this) );
            observer_ = 0;
        }
        
        void method()
        {
            scoped_lock lk(m_);
            not_used_ = false;
            .....
            if(observer_) // к этому нужно быть готовым.
                observer_->update();
            .....
            not_used_ = true;
            cv_.notify_all();
        }
    };

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

    Тогда вместо дедлока мы получаем спинлок (ждущий поток молотит цикл проверка-спячка). Его, по крайней мере, можно разрулить.
    ... << RSDN@Home 1.2.0 alpha rev. 655>>
  • Перекуём баги на фичи!
     
    Подождите ...
    Wait...
    Пока на собственное сообщение не было ответов, его можно удалить.