Параллелизм Потоков C++11, Ваш Технологический Велосипед (Apple) Gcd



Введение Добрый вечер хабровчане.

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

Более пяти лет занимаюсь разработкой игровых проектов на C++/Objective C++, в основном для платформы iOS. 2 года назад я решил попробовать свои силы в «нативной» разработке, используя только Objective-C. Примерно в то же время я заинтересовался технологией GCD от Apple (сразу после просмотра очередного WWDC).

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

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

Но довольно нетривиальная задача — сменить поток в конце операции загрузки на основной с целью дальнейшей загрузки во VRAM. Конечно, можно было закрыть глаза на эту проблему и использовать Shared Context для графического контекста, но растущий во мне на тот момент перфекционизм по отношению к собственному коду и дизайнерским решениям для графических систем не позволял мне этого сделать.

В общем, было решено попробовать GCD на «любимом» проекте, над которым я в то время работал.

И получилось довольно хорошо.

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

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

Большинство сотрудников не знакомы с Objective-C. Да и сам я не очень люблю этот язык (может быть, только за исключением его объектной модели, построенной по принципам языка Smalltalk).

Прочитав спецификации 11-го стандарта и изучив множество буржуазных блогов, я решил написать свой велосипед, аналогичный Apple CGD. Конечно, я не ставлю перед собой цели объять необъятность и ограничился лишь реализацией паттерна «Пул потоков» и возможностью выхода в любой момент из контекста вторичного потока в контекст основного потока, и наоборот. Для этого мне понадобились следующие нововведения C++11 — std::function, вариативные шаблоны и конечно же работа с std::thread. (std::shared_ptr используется только для личного спокойствия).

Конечно, еще одна цель, которую я перед собой ставлю — кроссплатформенность.

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

Но, немного изучив stackoverflow, я увидел, что эту проблему можно решить и установкой дополнительного пакета «Visual C++ October 2012 CTP».



Выполнение
Как я уже упоминал, эта идея основана на шаблоне Thread Pool. При проектировании были выделены два класса «gcdpp_t_task», которые агрегировали собственно исполняемую задачу и gcdpp_t_queue — очередь, накапливающую задачи.

  
  
  
  
  
  
  
   

template<class FUCTION, class. ARGS> class gcdpp_t_task { protected: FUCTION m_function; std::tuple<ARGS.> m_args; public: gcdpp_t_task(FUCTION _function, ARGS. _args) { m_function = _function; m_args = std::make_tuple(_args.); }; ~gcdpp_t_task(void) { }; void execute(void) { apply(m_function, m_args); }; };

Как мы видим, этот класс является шаблонным.

И это создает для нас проблему — как хранить задачи в одной очереди, если они разных типов? Давно задавался вопросом, почему до сих пор нет полноценной реализации интерфейсов/протоколов на C++.

В конце концов, принцип программирования более эффективен в абстракции, чем в реализации.

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



class gcdpp_t_i_task { private: protected: public: gcdpp_t_i_task(void) { }; virtual ~gcdpp_t_i_task(void) { }; virtual void execute(void) { assert(false); }; };

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

Давайте немного остановимся и посмотрим на класс gcdpp_t_task. Как я уже говорил, класс является шаблонным.

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

Он реализует только один метод выполнения, в котором функции передаются устаревшие параметры.

Вот тут-то и началась головная боль.

Как сохранить параметры таким образом, чтобы их можно было позже передать при отложенном вызове? На помощь пришло решение использовать std::tuple.

template<unsigned int NUM> struct apply_ { template<typename. F_ARGS, typename. T_ARGS, typename. ARGS> static void apply(std::function<void(F_ARGS. args)> _function, std::tuple<T_ARGS.> const& _targs, ARGS. args) { apply_<NUM-1>::apply(_function, _targs, std::get<NUM-1>(_targs), args.); } }; template<> struct apply_<0> { template<typename. F_ARGS, typename. T_ARGS, typename. ARGS> static void apply(std::function<void(F_ARGS. args)> _function, std::tuple<T_ARGS.> const&, ARGS. args) { _function(args.); } }; template<typename. F_ARGS, typename. T_ARGS> void apply(std::function<void(F_ARGS. _fargs)> _function, std::tuple<T_ARGS.> const& _targs) { apply_<sizeof.(T_ARGS)>::apply(_function, _targs); }

Ну вот вроде все стало прозрачно и понятно.

Теперь осталось только организовать «Пул потоков» с приоритетами.



class gcdpp_t_queue { private: protected: std::mutex m_mutex; std::thread m_thread; bool m_running; void _Thread(void); public: gcdpp_t_queue(const std::string& _guid); ~gcdpp_t_queue(void); void append_task(std::shared_ptr<gcdpp_t_i_task> _task); };

Вот реальный интерфейс, который реализует агрегацию и инкапсуляцию очереди задач.

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

Естественно, такие операции, как push и pop, заключаются в объект синхронизации мьютекса для безопасной работы в многопоточной среде.

Еще мне нужен был класс, реализующий аналогичный функционал, но работающий исключительно в основном потоке.

gcdpp_t_main_queue — скромнее по содержанию, так как более тривиален.

А теперь самое главное привести все это в более-менее рабочий вид.

class gcdpp_impl { private: protected: friend void gcdpp_dispatch_init_main_queue(void); friend void gcdpp_dispatch_update_main_queue(void); friend std::shared_ptr<gcdpp_t_queue> gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY _priority); friend std::shared_ptr<gcdpp_t_main_queue> gcdpp_dispatch_get_main_queue(void); template<class. ARGS> friend void gcdpp_dispatch_async(std::shared_ptr<gcdpp_t_main_queue> _queue, std::function<void(ARGS. args)> _function, ARGS. args); std::shared_ptr<gcdpp_t_main_queue> m_mainQueue; std::shared_ptr<gcdpp_t_queue> m_poolQueue[gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_MAX]; static std::shared_ptr<gcdpp_impl> instance(void); std::shared_ptr<gcdpp_t_queue> gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY _priority); std::shared_ptr<gcdpp_t_main_queue> gcdpp_dispatch_get_main_queue(void); template<class. ARGS> void gcdpp_dispatch_async(std::shared_ptr<gcdpp_t_main_queue> _queue, std::function<void(ARGS. args)> _function, ARGS. args); public: gcdpp_impl(void); ~gcdpp_impl(void); };

Класс gcdpp_impl является одноэлементным и полностью инкапсулирован от внешних влияний.

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

Класс также содержит 5 дружественных функций.

Функции gcdpp_dispatch_init_main_queue и gcdpp_dispatch_update_main_queue являются паразитами.

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

gcdpp_dispatch_update_main_queue — функции обработки задач в основном потоке.

и очень хочется избавить пользователя от встраивания этой функции в его Run Loop. С остальными функциями вроде всё прозрачно: gcdpp_dispatch_get_global_queue — получает очередь по приоритету; gcdpp_dispatch_get_main_queue — получает очередь в основном потоке; gcdpp_dispatch_async — ставит операцию для отложенного вызова в определенный поток, в определенную очередь.



Приложение
И зачем все это нужно? Попробую показать выгоду от этой реализации с помощью нескольких тестов:

std::function<void(int, float, std::string)> function = [](int a, float b, const std::string& c) { std::cout<<<<a<<b<<c<<std::endl; }; gcdpp::gcdpp_dispatch_async<int, float, std::string>(gcdpp::gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_HIGH), function, 1, 2.0f, "Hello World");

В этом примере функция, объявленная в лямбда-выражении, будет вызываться отложенной в потоке с высоким приоритетом.



class Clazz { public: int m_varible; void Function(int _varible) { m_varible = _varible; }; }; std::shared_ptr<Clazz> clazz = std::make_shared<Clazz>(); clazz->m_varible = 101; std::function<void(std::shared_ptr<Clazz> )> function = [](std::shared_ptr<Clazz> clazz) { std::cout<<"call"<<clazz->m_varible<<std::endl; }; gcdpp::gcdpp_dispatch_async<std::shared_ptr<Clazz>>(gcdpp::gcdpp_dispatch_get_global_queue(gcdpp::GCDPP_DISPATCH_QUEUE_PRIORITY_HIGH), function, clazz);

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



void CParticleEmitter::_OnTemplateLoaded(std::shared_ptr<ITemplate> _template) { std::function<void(void)> function = [this](void) { std::shared_ptr<CVertexBuffer> vertexBuffer = std::make_shared<CVertexBuffer>(m_settings->m_numParticles * 4, GL_STREAM_DRAW); .

m_isLoaded = true; }; thread_concurrency_dispatch(get_thread_concurrency_main_queue(), function); }

И самый важный тест — вызов операции в основном потоке из вторичного потока.

Функция _OnTemplateLoaded вызывается из фонового потока, который анализирует XML-файл с настройками.

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

Заключение
В целом проблема была решена в рамках поставленных целей.

Конечно, есть еще много недоделанного и непроверенного, но пока во мне горит искра, я буду продолжать совершенствовать свою реализацию НОД.

На этот проект было потрачено около 18 часов работы, в основном жертвуемых во время рабочих перекуров.

Исходные коды можно найти в открытом доступе.

исходный код .

Проект пока не запихнут под VS 2012, но думаю скоро он там появится.

P.S. Жду адекватной критики.

Теги: #c++11 #НОД #программирование #программирование #C++

Вместе с данным постом часто просматривают: