Дизайн пула потоков
От: Alu Россия  
Дата: 27.08.09 07:04
Оценка:
Добрый день.
Разрабатываю свой простой пул потоков. О существовании готовых профессиональных реализаций знаю. Цель — практически Just For Fun.
Дизайн хотелось бы иметь в стиле Qt::QThread и Java.
Первый вариант:
class IRunable
{
public:
    virtual void Run() = 0;
    virtual ~IRunable();
};

void Start(IRunable* _pcRunable); // < запускает метод Run() _pcRunable в отдельном потоке.

Т.е. пользователь пула потоков просто реализует интерфейс IRunable, переопределённый метод Run() будет запущен в отдельном потоке:
class CSomeRunable : public IRunable
{
    virtual void Run(); // < Код пользователя, запускаемый в отдельном потоке.
    void AdditionalMethod();
};

Вопрос: Как корректно освободить ресурсы CSomeRunable, ведь такой объект будет использоваться двумя нитями сразу: нитью создающей объект и нитью исполняющей метод CSomeRunable::Run()
int main()
{
    CSomeRunable* p = new CSomeRunable();
    Start(p); // < Запуск  CSomeRunable::Run() в отдельной нити.

    // ....

    p->AdditionalMethod();

    // ....

    delete p; /// Некорректно! Нет гарантии что объект не используется другой нитью.
    
    return 0;
}

Недостатки:
При таком дизайне нужен отдельный активный механизм, который будет отслеживать завершился ли дочерний поток и когда тот завершится — удалит объект. Что-то типа активного сборщика мусора.
Второй вариант:
class IRunable
{
public:
    virtual void Run() = 0;
    virtual IRunable* Сlone() = 0; // < Создаёт копию объекта
    virtual ~IRunable() ;
};

void Start(const IRunable& _rcRunable); // < Создаёт копию _rcRunable и запускает метод Run() копии в отдельном потоке.
                                        // По завершении Run(), копия удаляется.

Недостатки:
1. Пользователю прийдётся реализовывать метод Сlone(), что может быть непросто для сложных объектов, имеющих средства синхронизации.
2. Пользователь теряет связь с активным объектом. Т.е. копия объекта должна сама сообщить свой адрес, чтобы пользователь мог с ней взаимодействовать из создающей нити.

Третий вариант:
Использовать потоковозащищённые умные указатели.
Недостатки:
Прийдётся тащить их из сторонних библиотек.

Четвёртый вариант:
Отказаться от IRunable в пользу указателей на ф-ии.
Недостатки: необъектно и подедовски

Буду благодарен как за модификации моих вариантов, так и за принципиально новые, с другим дизайном.
Спасибо.
Настоящему индейцу завсегда везде ништяк!
pool threads multithreading
Re: Дизайн пула потоков
От: Сергей Мухин Россия  
Дата: 27.08.09 07:14
Оценка:
Здравствуйте, Alu, Вы писали:

и не забыть обезопасить себя от такого ошибочного использования

int main()
{
    CSomeRunable p;
    Start(&p);
}



Alu>Четвёртый вариант:

Alu>Отказаться от IRunable в пользу указателей на ф-ии.
Alu>Недостатки: необъектно и подедовски

Это субъективные недостатки, а есть объективные?
---
С уважением,
Сергей Мухин
Re: Дизайн пула потоков
От: Аноним  
Дата: 27.08.09 08:08
Оценка:
Не надо никаких сборщиков мусора. Пусть поток и удаляет объект когда его поток завершиться. А еще совершенно непонятно накуя нужна отдельная функция Start() в глобальной неймспейсе, почему она не мембер класса?
Re: Дизайн пула потоков
От: Alexander G Украина  
Дата: 27.08.09 08:09
Оценка: 1 (1)
Здравствуйте, Alu, Вы писали:

Alu>Третий вариант:

Alu>Использовать потоковозащищённые умные указатели.
Alu>Недостатки:
Alu>Прийдётся тащить их из сторонних библиотек.

3.1. Самому сделать потокобезопасный подсчёт ссылок, вручную дёргать соответствующие AtomicIncrement/AtomicDecrement.
Русский военный корабль идёт ко дну!
Re: Дизайн пула потоков
От: ioni Россия  
Дата: 27.08.09 08:12
Оценка: +1
Здравствуйте, Alu, Вы писали:

Вообще то это не пулл потоков
Re: Дизайн пула потоков
От: Кодт Россия  
Дата: 27.08.09 11:07
Оценка: 3 (1)
Здравствуйте, Alu, Вы писали:

Alu>Четвёртый вариант:

Alu>Отказаться от IRunable в пользу указателей на ф-ии.
Alu>Недостатки: необъектно и подедовски

Как раз необъектость может оказаться достоинством.
Что такое поток? Это некоторая функция, выполняемая асинхронно.

Вся затея с IRunnable (с единственным методом Run) делается лишь затем, чтобы реализовать замыкание функции средствами языка, изначально не поддерживающего замыкания.
Поэтому и говорят: "объекты — замыкания для бедных".

Есть два основных способа организации потоков
1) поток — это функция с параметрами; через эти параметры функция узнаёт, как именно можно связаться с другими потоками — например, посылать сообщения
2) поток — это функция с общими данными; этими данными совместно владеют: сам поток и, как минимум, тот, кто его запустил

Связанный с потоком объект — это как раз второй случай.

Дальше встаёт вопрос — как грамотно реализовать владение объектом.
— Договориться о времени жизни и оставить владение объектом за пределами рабочего потока. Хозяин, перед тем, как убить объект, ждёт завершения потока.
— Договориться и отдать владение объектом рабочему потоку. Создатель после запуска потока уже не вправе безусловно рассчитывать на валидность объекта, и работает с ним как-то косвенно. Это, на самом деле, (1); объект является теми самыми параметрами функции и контекстом её исполнения, и более ничем.
— Сделать честное совместное владение — на подсчёте ссылок. Один владелец — это создатель, другой владелец — поток. Когда оба отпустят, тогда объект и умрёт.


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

И если первые две ещё есть смысл инкапсулировать в один объект, то пользовательские данные — это уже необязательно.


Это я всё плавно подвожу к тому, что есть такая библиотека Boost.Thread, с которой можно брать пример.
А не с явы.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re[2]: Дизайн пула потоков
От: Аноним  
Дата: 27.08.09 13:27
Оценка:
К>Дальше встаёт вопрос — как грамотно реализовать владение объектом.
К>- Договориться о времени жизни и оставить владение объектом за пределами рабочего потока. Хозяин, перед тем, как убить объект, ждёт завершения потока.
К>- Договориться и отдать владение объектом рабочему потоку. Создатель после запуска потока уже не вправе безусловно рассчитывать на валидность объекта, и работает с ним как-то косвенно. Это, на самом деле, (1); объект является теми самыми параметрами функции и контекстом её исполнения, и более ничем.
К>- Сделать честное совместное владение — на подсчёте ссылок. Один владелец — это создатель, другой владелец — поток. Когда оба отпустят, тогда объект и умрёт.
Можно еще так — потоком полностью владеет объект, а не наоборот. Соответственно прежде чем объект удаляется он сам ждет когда завершится поток который он же сам и стартанул для себя самого. Владелец объекта работает лишь с объектом, а не с двумя сущностями одновременно.
Re[3]: Дизайн пула потоков
От: Аноним  
Дата: 27.08.09 13:41
Оценка:
ну и примерная реализация.. както так:
class BaseThread
{

static unsigned int __stdcall StaticThreadRoutine(void*p)
{
 ((BaseThread*)p)->ThreadRoutine();
 return 0;
}

protected:
//overload in descedant
virtual void ThreadRoutine() = 0;

//use in descedant's code
virtual bool IsCheckStopped(DWORD wait = 0) {return ::WaitForSingleObject(_stop, wait)==WAIT_OBJECT_0;}

//don't publish in descedants
virtual ~BaseThread()
{
 assert(_thread==NULL);
 if (_stop) ::CloseHandle(_stop);
}

//start object's thread
void Start()
{
assert(!_thread);
if (!_stop) _stop = ::CreateEvent(0, 1, 0, 0); else ::ResetEvent(_stop);
_thread = (HANDLE)_beginthreadex(..., StatisThreadRoutine, this, ...);
}

//stop object's thread
void Stop()
{
assert(_thread && _stop);
::SetEvent(_stop);
::WaitForSingleObject(_thread, INFINITE);
::CloseHandle(_thread);
_thread = NULL;
}

public:

//use to delete object instance
virtual void Release()
{
if (_thread) Stop();
delete this;
}

void Activate()
{
if (!_thread) Start();
}

};


disclamer: компилять не пробовал
Re[3]: Дизайн пула потоков
От: Кодт Россия  
Дата: 27.08.09 13:52
Оценка:
Здравствуйте, <Аноним>, Вы писали:

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


Активный объект?
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re[4]: Дизайн пула потоков
От: Аноним  
Дата: 27.08.09 14:09
Оценка:
А>>Можно еще так — потоком полностью владеет объект, а не наоборот. Соответственно прежде чем объект удаляется он сам ждет когда завершится поток который он же сам и стартанул для себя самого. Владелец объекта работает лишь с объектом, а не с двумя сущностями одновременно.
К>Активный объект?
А это так называется? Сенкс, буду знать.
Re: Дизайн пула потоков
От: demi США  
Дата: 27.08.09 14:33
Оценка:
Здравствуйте, Alu, Вы писали:

Alu>Первый вариант:

Alu>
Alu>class IRunable
Alu>{
Alu>public:
Alu>    virtual void Run() = 0;
Alu>    virtual ~IRunable();
Alu>};
Alu>


Alu>void Start(IRunable* _pcRunable); // < запускает метод Run() _pcRunable в отдельном потоке.


Как вам такой вариант: добавить метод Clone
class IRunable
{
public:
    virtual void Run() = 0;
    virtual IRunable* Clone() = 0;
    virtual ~IRunable();
};

И реализовать Start чуть по-другому:
void Start(IRunable* _pcRunable)
{
    // Теперь у нас есть копия, за временем жизни которой мы можем управлять.
    IRunable* pRunable = _pcRunable->Clone();
    // ... 
}


Ну или конечно счетчик ссылок. Конкретных способов реализации — масса.
Не стыдно попасть в дерьмо, стыдно в нём остаться!
Re[4]: Дизайн пула потоков
От: Кодт Россия  
Дата: 28.08.09 09:49
Оценка: +1
Здравствуйте, <Аноним>, Вы писали:

А>ну и примерная реализация.. както так:


Очень полезно добавить рандеву на старте.
Эскиз:
class Thread
{
    // thread_xxxxx - функции, исполняемые в контексте потока
    
    // интерфейс от потомков
    virtual void thread_prolog() throw();
    virtual void thread_body();
    virtual void thread_epilog() throw();
    
    void thread_outline()
    {
        // пролог-эпилог можно завернуть в scope guard
        thread_prolog();
        try
        {
            thread_notify_started();
            thread_body();
        }
        catch(StopSignal)
        {
        }
        catch(...)
        {
        }
        thread_epilog();
    }
    
    Handle m_thrSlave;
    static int thread_thunk(void* p)
    {
        static_cast<Thread*>(p)->thread_outline();
        return 0;
    }

    volatile bool m_resume, m_stop;
    Event m_evtMaster, m_evtSlave;
    
    void thread_notify_started()
    {
        m_evtSlave.set();
        while(true)
        {
            thread_check();
            if(m_resume)
                break;
            m_evtMaster.wait(INFINITE);
        }
    }
    
    void thread_check()
    {
        if(m_stop)
            throw StopSignal;
    }
public:
    void start(bool resumed)
    {
        m_thrSlave = begin_thread(thread_thunk, this);
        m_evtSlave.wait(INFINITE);
        
        if(resumed)
            resume();
    }
    void resume()
    {
        m_resume = resumed;
        m_evtMaster.set();
    }
    void stop(int timeout)
    {
        m_stop = true;
        if(m_thrSlave)
            m_thrSlave.wait(timeout);
    }
};


Это нужно для того, чтобы передать потоку владение объектом (самим собой).
class SharedThread
{
    weak_ptr<SharedThread> m_weakMyself;
    shared_ptr<SharedThread> m_strongMyself; // владеет собой внутри потока
    
    void thread_prolog() { m_strongMyself = myself(); }
    void thread_epilog() { m_strongMyself.reset(); }

public:
    SharedThread() : m_weakMyself(this) {}
    shared_ptr<SharedThread> myself() { return m_weakMyself; }
};

.....
shared_ptr<SharedThread> p (new MySharedThread().myself());
p->start(true);
p.reset();
.....
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re[5]: Дизайн пула потоков
От: Аноним  
Дата: 28.08.09 11:01
Оценка:
К>Это нужно для того, чтобы передать потоку владение объектом (самим собой).
Разбираться в коде влом, но не надо потоку владеть объектом. Объект владеет потоком, а не наоборот.
Если хочется чтобы объект автоматически удалялся при разрушении потока — тогда да, зависимость следует развернуть на 180 градусов. А делать двухсторонюю зависимость очень некрасиво.
Re[6]: Дизайн пула потоков
От: Кодт Россия  
Дата: 28.08.09 12:41
Оценка:
Здравствуйте, <Аноним>, Вы писали:

А>Разбираться в коде влом, но не надо потоку владеть объектом. Объект владеет потоком, а не наоборот.


Объект владеет потоком, а кто-то владеет объектом.
Тогда нужно перед уничтожением объекта убедиться, что поток остановлен.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re[2]: Дизайн пула потоков
От: Alu Россия  
Дата: 28.08.09 12:41
Оценка:
Здравствуйте, Сергей Мухин, Вы писали:

СМ>Это субъективные недостатки, а есть объективные?


Отсутствие ОО подхода для меня есть объективный минус.
Но как справедливо заметил Кодт, природа потока ближе к ф-ии чем к объекту.
Настоящему индейцу завсегда везде ништяк!
Re[2]: Дизайн пула потоков
От: Alu Россия  
Дата: 28.08.09 12:45
Оценка:
Здравствуйте, Аноним, Вы писали:

А>А еще совершенно непонятно накуя нужна отдельная функция Start() в глобальной неймспейсе, почему она не мембер класса?


На самом деле она мембер. Есть и другие, "сервисные" мембры для работы с потоком. Но для того чтобы сделать примеры более локоничными и понятными — описал их так
Настоящему индейцу завсегда везде ништяк!
Re[7]: Дизайн пула потоков
От: Аноним  
Дата: 28.08.09 12:46
Оценка:
А>>Разбираться в коде влом, но не надо потоку владеть объектом. Объект владеет потоком, а не наоборот.
К>Объект владеет потоком, а кто-то владеет объектом.
К>Тогда нужно перед уничтожением объекта убедиться, что поток остановлен.
Для этого у объекта есть деструктор или, лучше, (в связи с особенностями наследования в С++) какой нить иной метод удаления, где сам объект может спокойно остановить поток, прежде чем подохнет. Только не говорите мне что вы удаляете объекты путем прямого вызова free на них
Тому кто владеет объектом достаточно знать семантиру работы именно с объектом. А внутренними ресурсами которыми пользуется объект — память, файлы, сетевые подключения _и_ потоки — про них оставьте заботиться самому объекту.
Re[2]: Дизайн пула потоков
От: Alu Россия  
Дата: 28.08.09 12:47
Оценка:
Здравствуйте, ioni, Вы писали:

I>Вообще то это не пулл потоков


Вы правы, это внешний итерфейс к нему. После вызова Start() Runable-объект передаётся одной из нитей пула. Она его исполняет и возвращается в пул.
Настоящему индейцу завсегда везде ништяк!
Re[2]: Дизайн пула потоков
От: Alu Россия  
Дата: 28.08.09 12:51
Оценка:
Здравствуйте, demi, Вы писали:

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


D>Как вам такой вариант: добавить метод Clone


Это и есть вариант №2 См. первое сообщение. Там же описаны недостатки.
Настоящему индейцу завсегда везде ништяк!
Re: Дизайн пула потоков
От: Alexey Frolov Беларусь  
Дата: 28.08.09 13:09
Оценка: 1 (1)
Здравствуйте, Alu, Вы писали:

Поток не должен управлять временем жизни объекта. В общем случае (а мы говорим про пул потоков), поток не завершается а фактически выбирает из очереди очередной Irunable и вызывает метод Run и так далее пока его не остановят. А вот управлять временем жизни объекта в рамках данной реализации я вижу ровно три способа.
Первый: объект IRunable автоматический, существует глобально, уничтожается гарантированно после пула потоков, либо после того как им уже никто не пользуется, регулируется дизайном, например

{
   CSomeRunnable work;
   CThreadPool pool;
   pool.Init();
   pool.Start(&work);
   pool.Shutdown();
}


Второй: объект одноразовый, создается динамически, запускается в пул, уничтожается самостоятельно, отработав свою порцию


class CSomeRunable : public IRunable
{
   virtual void Run() 
   {
      // execute any work

      delete this;
   }
};

{
   pool.Start(new CSomeRunnable());
}



Третий: похож на второй метод за исключением того что производится подсчет ссылок и автоматическое удаление объекта произойдет тогда когда на объект больше никто не ссылается, то есть RefCount=0
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.