LINUX.ORG.RU

Тредпул и таски, таски, таски

 , ,


0

3

Все никак не додумаю правильную архитектуру отвечающую целям.

База:

Приложение будет выполнять различные по типу таски.

Каждый таск перед его запуском будет отправлен в тредпул и в итоге выполнен в каком-то треде.

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

На завершение таска можно подписаться сигналом/каллбеком.

С выше описаным у меня проблем и непоняток нет. Едем дальше.

Цели:

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

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

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

В целом, это тоже не вызывает проблем в реализации.

Подводные камни:

Вижу два пути:

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

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

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

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

Мьютексы/фьютексы/семафоры, да. Но, взяв самое медленное (мьютекс) возникает вопрос - не будут ли блокировки медленнее созданий-удалений?

Кстати, первый вариант не отменяет необходимость блокировки. Пусть лишь в меньшем количестве мест.

Комплексно:

Свободные таски надо хранить в списке, по списку надо бежать, искать.

Принимать решение о создании таска нужного типа если нет еще ни одного такого или все такие сейчас заняты.

Чтобы узнать свободен ли таск нужно его сначала залочить. И так каждый в цикле.

Делать много списков где каждый хранит только конкретный тип (а типов дохрена, на крайняк группу близких типов?) тасков?

И, думается мне, это не преждевременная оптимизация, а лишь желание сделать сразу правильно, чтобы потом не переписывать много.

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

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

Дай колбаски хлеб доесть, а?

pon4ik

★★★★★

Последнее исправление: deep-purple (всего исправлений: 1)

Ответ на: комментарий от anonymous

Вынужден признать, что среди остального это единственно адекватный ответ!

// другой анонимус

anonymous
()
Ответ на: комментарий от pon4ik

тредпул и так есть - в нем же очередь до отправки в тред и приоритеты, он только принимает таски и внутри все делает сам.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от Kuzy

во и у меня есть планировщик-манагер, которвй тоже что-то там решает

deep-purple ★★★★★
() автор топика

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

Создаваемая на каждый чих таска - это ведь std::function от std::bind, и наполнено оно в основном смартпоинтерами, да? Не понятно, почему аллокация под неё и инициалиазция считаются тяжеловесными.

Имхо, ты зря загоняешься с пулом тасок, оно того не стоит.

Manhunt ★★★★★
()
Последнее исправление: Manhunt (всего исправлений: 1)

Как насчет того, чтобы обойтись минималистичным адаптером над boost::asio::thread_pool и не клевать никому мозги?

Manhunt ★★★★★
()

Есть такая модная библиотека для C++ называется .Net Core

lovesan ★★
()
Ответ на: комментарий от Manhunt

а кто сказал что я считаю тяжеловесным именно это? я сказал что стою на распутье и спрашивал что тяжелее из двух вариантов.

да, по сути это одна функция, вернее метод класса, потому как там еще хранятся каллбеки и накапливаются данные (от сабтасков) если мы ждем сабтаски, ну и состояния и блокировки там же.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от Manhunt

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

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от deep-purple

Создаваемая на каждый чих таска - это ведь std::function от std::bind, и наполнено оно в основном смартпоинтерами, да?

да, по сути это одна функция, вернее метод класса, потому как там еще хранятся каллбеки и накапливаются данные (от сабтасков) если мы ждем сабтаски, ну и состояния и блокировки там же

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

тредпул с очередью и приоритетами

У бустового тредпула очередь есть. А приоритеты .. может, можно обойтись фиксированным количеством приоритетов (например: hi, medium, low), и под каждый приоритет тупо поднять отдельный тредпул?

Manhunt ★★★★★
()
Ответ на: комментарий от Manhunt

не так. каллбеки не там.

у меня сейчас задумано так:

контроллер принимает сигнал начать какойто таск и просит таскманагера дать ему нужный таск, сетит ему цель и отправляет в тредпул.

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

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

калбек это тоже обьект, маленький, он хранит указатель на родительский таск и индекс по которому родитель ждет данные от сабтаска.

по завершении работы сабтаск обходит все калбеки и через них вызывает родительские таски передав им результат и статус своей работы.

deep-purple ★★★★★
() автор топика
Последнее исправление: deep-purple (всего исправлений: 1)
Ответ на: комментарий от Manhunt

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

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от deep-purple

больше одного тредпула смысла пока не вижу

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

Manhunt ★★★★★
()
Ответ на: комментарий от Manhunt

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

я не вижу простого решения по синхронизации тасков в таскманагере и между собой вне контекста тредпула.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от deep-purple

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

Угу, это можно делать посредством коллбэков. И роль таскманагера тут вырожденная.

К примеру, есть таска #1 - приготовить пирог. Чтобы пирог приготовить, сперва нужно поискать ингридиенты, так что вместо таски #1 создаём подтаски для поиска ингридиентов #2 и #3. Поиск ингридиентов выполняется наперегонки. Когда все ингридиенты найдены (обе таски #2 и #3 завершились), можно заняться собственно готовкой - это будет таска #4. Когда готовка завершена - можно уведомиьт заказчика таски #1, что всё готово.

Накидал тут на коленке (скорее всего код бажный, но идею +/- иллюстрирует):

#include <functional>
#include <memory>
#include <atomic>
#include <iostream>
#include <boost/asio.hpp>

std::unique_ptr<boost::asio::thread_pool> g_pool;

template<class Task>
void schedule(Task task)
{
	boost::asio::post(*g_pool, task);
}

template<typename Arg1, typename Arg2>
class CollectAndSchedule: public std::enable_shared_from_this< CollectAndSchedule<Arg1, Arg2> >
{
	typedef CollectAndSchedule<Arg1, Arg2> This;
	typedef std::function<void(Arg1, Arg2)> Task;

	std::atomic<int> args_count;
	Task task;
	Arg1 arg1;
	Arg2 arg2;

	void run_task() { task(arg1, arg2); }
	void set_arg1(Arg1 arg) { arg1 = arg; on_arg_set(); }
	void set_arg2(Arg2 arg) { arg2 = arg; on_arg_set(); }
	void on_arg_set()
	{
		if(++args_count == 2)
		{
			schedule(std::bind(&This::run_task, this->shared_from_this()));
		}
	}

public:
	template<class T>
	CollectAndSchedule(T t): args_count(0), task(t) {}
	std::function<void(Arg1)> make_arg1_callback()
	{
		using namespace std::placeholders;
		return std::bind(&This::set_arg1, this->shared_from_this(), _1);
	}
	std::function<void(Arg2)> make_arg2_callback()
	{
		using namespace std::placeholders;
		return std::bind(&This::set_arg2, this->shared_from_this(), _1);
	}
};

struct Cat{ };
struct Dog{ void eat(Cat) {} };
struct Pie{ void cook(Cat, Dog) {} };

// a task
void catch_cat(std::function<void(Cat)> result_cb)
{
	Cat cat; // find a cat
	std::cout << "[Task #2] Got a cat!" << std::endl;
	result_cb(cat); // pass the cat to consumer
}

// a task
void catch_dog(std::function<void(Dog)> result_cb)
{
	Dog dog; // find a dog
	std::cout << "[Task #3] Got a dog!" << std::endl;
	result_cb(dog); // pass the dog to consumer
}

// a task
void bloody_business(std::function<void(Pie)> result_cb, Cat cat, Dog dog)
{
	Pie pie;
	std::cout << "[Task #4] Cooking a pie!" << std::endl;
	pie.cook(cat, dog);
	result_cb(pie); // pass the pie to consumer
}

// a task
void make_pie(std::function<void(Pie)> result_cb)
{
	std::cout << "[Task #1] Got an order for a pie, but need ingridients first!" << std::endl;
	using namespace std::placeholders;
	auto subtasks_joiner = std::make_shared< CollectAndSchedule<Cat, Dog> >(
		std::bind(bloody_business, result_cb, _1, _2)
	);
	schedule(std::bind(catch_cat, subtasks_joiner->make_arg1_callback()));
	schedule(std::bind(catch_dog, subtasks_joiner->make_arg2_callback()));
}

void happy_with_a_pie(Pie)
{
	std::cout << "Got a pie!" << std::endl;
}

int main()
{
	// init
	g_pool = std::make_unique<boost::asio::thread_pool>();

	schedule(std::bind(make_pie, happy_with_a_pie));

	// shutdown
	g_pool->join();
	g_pool.reset();
}

Manhunt ★★★★★
()
Последнее исправление: Manhunt (всего исправлений: 1)
Ответ на: комментарий от deep-purple

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

Проблема состоит в том, что конкурентный доступ к памяти влечёт за собой нулевой либо отрицательный эффект от распараллеливания. По этой причине использовать под очередь задач какой-нибудь std::priority_queue было бы крайне глупо.

Кстати, выше по треду правильно говорят, что идея делать много мелких тасок - глупая. По той же самой причине.

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

Manhunt ★★★★★
()
Ответ на: комментарий от deep-purple

надо понимать как оно работает

наилучшей идеей тогда будет глянуть в исходники готового - boost-овый вариант выше советуют, вот его, например

и не надо бояться смотреть в исходники (хотя, конечно, именно от исходников буста и std в gcc глаза вытекают)

next_time ★★★★★
()
Ответ на: комментарий от deep-purple

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

Вы можете взять любой плюсовый акторный фреймворк и, вероятно, у вас получится отобразить свои таски на тамошних акторов без особых проблем. И вам не придется думать о пулах потоках, шедулерах и пр.

Поскольку акторы могут отображаться на CSP-процессы, то аналогичное решение вы можете получить и на stateful-короутинах. Каждый таск – это короутина, она создает channel (или несколько channel-ов) для ответов от дочерних тасков, запускает эти дочерние таски (опять же в виде короутин), после чего ждет на channel-ах результатов.

Можно взять Boost.Fiber и, вероятно, у вас без проблем получится отобразить свои таски на тамошние файберы, а каналы из Boost.Fiber вы сможете использовать для взаимодействия между тасками.

eao197 ★★★★★
()
Ответ на: комментарий от Manhunt

но у тебя тут появилась сущность которой нужно знать не только о верхнем таске, но и о сабтасках, у меня же о своих зависимостях знает только сам таск.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от deep-purple

но у тебя тут появилась сущность которой нужно знать не только о верхнем таске, но и о сабтаска

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

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

Manhunt ★★★★★
()
Ответ на: комментарий от Manhunt

конкурентный доступ к памяти где данные? они в основном не пересекаются. есть правда пара узких мест, но это терпимо.

мелких тасок потому, что вызовы вверх подниматься будут от сабтасков к таскам - вот тебе и большие.

давай снова про пирог:

есть таск приготовить пирог, он «верхний». в его реализации есть вызовы тасков: сходи в магазин за ингридиентами пирога, подготовь ингридиенты, испеки. вот эти три явно только последовательно могут быть выполнены.

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

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

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

теперь верхний таск может отчитаться тому кто его вызывал.

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

само главное - таск, отправивший в работу сабтаск сам уже в треде отработал и будет дорабатывать только за счет довызовов через калбеки.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от Manhunt

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

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от eao197

или опять же подглядеть как устроено у акторов и допилить свой колхоз под этот принцип.

да - тред однозначно полезен, но я еще ничего не читал вообще т.к. инета дома до сих нет, там авария какая-то, а на работе некогда чтивом заниматься.

deep-purple ★★★★★
() автор топика
Ответ на: комментарий от trashymichael

бро, или раскрой мысль по нормальному, с аргументами и советом как сделать правильно, или ехай отседа формошлепить.

deep-purple ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.