Добрый день.
Разрабатываю свой простой пул потоков. О существовании готовых профессиональных реализаций знаю. Цель — практически 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 в пользу указателей на ф-ии.
Недостатки: необъектно и подедовски
Буду благодарен как за модификации моих вариантов, так и за принципиально новые, с другим дизайном.
Спасибо.
и не забыть обезопасить себя от такого ошибочного использования
int main()
{
CSomeRunable p;
Start(&p);
}
Alu>Четвёртый вариант: Alu>Отказаться от IRunable в пользу указателей на ф-ии. Alu>Недостатки: необъектно и подедовски
Это субъективные недостатки, а есть объективные?
---
С уважением,
Сергей Мухин
Re: Дизайн пула потоков
От:
Аноним
Дата:
27.08.09 08:08
Оценка:
Не надо никаких сборщиков мусора. Пусть поток и удаляет объект когда его поток завершиться. А еще совершенно непонятно накуя нужна отдельная функция Start() в глобальной неймспейсе, почему она не мембер класса?
Здравствуйте, 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); объект является теми самыми параметрами функции и контекстом её исполнения, и более ничем. К>- Сделать честное совместное владение — на подсчёте ссылок. Один владелец — это создатель, другой владелец — поток. Когда оба отпустят, тогда объект и умрёт.
Можно еще так — потоком полностью владеет объект, а не наоборот. Соответственно прежде чем объект удаляется он сам ждет когда завершится поток который он же сам и стартанул для себя самого. Владелец объекта работает лишь с объектом, а не с двумя сущностями одновременно.
Здравствуйте, <Аноним>, Вы писали:
А>Можно еще так — потоком полностью владеет объект, а не наоборот. Соответственно прежде чем объект удаляется он сам ждет когда завершится поток который он же сам и стартанул для себя самого. Владелец объекта работает лишь с объектом, а не с двумя сущностями одновременно.
Активный объект?
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re[4]: Дизайн пула потоков
От:
Аноним
Дата:
27.08.09 14:09
Оценка:
А>>Можно еще так — потоком полностью владеет объект, а не наоборот. Соответственно прежде чем объект удаляется он сам ждет когда завершится поток который он же сам и стартанул для себя самого. Владелец объекта работает лишь с объектом, а не с двумя сущностями одновременно. К>Активный объект?
А это так называется? Сенкс, буду знать.
void Start(IRunable* _pcRunable)
{
// Теперь у нас есть копия, за временем жизни которой мы можем управлять.
IRunable* pRunable = _pcRunable->Clone();
// ...
}
Ну или конечно счетчик ссылок. Конкретных способов реализации — масса.
Не стыдно попасть в дерьмо, стыдно в нём остаться!
Это нужно для того, чтобы передать потоку владение объектом (самим собой).
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 градусов. А делать двухсторонюю зависимость очень некрасиво.
Здравствуйте, Аноним, Вы писали:
А>А еще совершенно непонятно накуя нужна отдельная функция Start() в глобальной неймспейсе, почему она не мембер класса?
На самом деле она мембер. Есть и другие, "сервисные" мембры для работы с потоком. Но для того чтобы сделать примеры более локоничными и понятными — описал их так
Настоящему индейцу завсегда везде ништяк!
Re[7]: Дизайн пула потоков
От:
Аноним
Дата:
28.08.09 12:46
Оценка:
А>>Разбираться в коде влом, но не надо потоку владеть объектом. Объект владеет потоком, а не наоборот. К>Объект владеет потоком, а кто-то владеет объектом. К>Тогда нужно перед уничтожением объекта убедиться, что поток остановлен.
Для этого у объекта есть деструктор или, лучше, (в связи с особенностями наследования в С++) какой нить иной метод удаления, где сам объект может спокойно остановить поток, прежде чем подохнет. Только не говорите мне что вы удаляете объекты путем прямого вызова free на них
Тому кто владеет объектом достаточно знать семантиру работы именно с объектом. А внутренними ресурсами которыми пользуется объект — память, файлы, сетевые подключения _и_ потоки — про них оставьте заботиться самому объекту.
Поток не должен управлять временем жизни объекта. В общем случае (а мы говорим про пул потоков), поток не завершается а фактически выбирает из очереди очередной Irunable и вызывает метод Run и так далее пока его не остановят. А вот управлять временем жизни объекта в рамках данной реализации я вижу ровно три способа.
Первый: объект IRunable автоматический, существует глобально, уничтожается гарантированно после пула потоков, либо после того как им уже никто не пользуется, регулируется дизайном, например
Второй: объект одноразовый, создается динамически, запускается в пул, уничтожается самостоятельно, отработав свою порцию
class CSomeRunable : public IRunable
{
virtual void Run()
{
// execute any workdelete this;
}
};
{
pool.Start(new CSomeRunnable());
}
Третий: похож на второй метод за исключением того что производится подсчет ссылок и автоматическое удаление объекта произойдет тогда когда на объект больше никто не ссылается, то есть RefCount=0