Введение Добрый вечер хабровчане.
В этой статье я хочу описать проблемы работы в многопоточной среде, с которыми я столкнулся, и пути их решения.
Более пяти лет занимаюсь разработкой игровых проектов на 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++
-
Как Работает Мультиподпись В Биткойне?
19 Oct, 24 -
Рынок Носимых Устройств Вырос Почти На 700%
19 Oct, 24 -
Введение В Рекомендательные Системы
19 Oct, 24