Introduction
Good evening habrovchane. In this article I want to describe the problems of working in a multithreaded environment, with whom I met and the ways to solve them. For more than five years I have been developing game projects in C ++ / Objective C ++, based on the iOS platform. 2 years ago I decided to try myself in “native” development using only Objective-C. Around the same time, I was interested in Apple's GCD technology (just after watching the next WWDC). First of all, in this technology I was attracted by the flexible ability to delegate transactions between threads. A fairly common task is to download any game resources in a low-priority thread. But a rather non-trivial task is to change the stream at the end of the download operation to the main stream in order to further load into VRAM. Of course, it was possible to turn a blind eye to this problem and use the Shared Context for the graphic context, but the growing awareness of my own code and solutions for designing graphic systems at that time did not allow me to do this. In general, it was decided to test GCD on a “pet” project, which I was working on at that very time. And it turned out pretty not bad. In addition to the tasks of solving the loading of game resources, I began to use GCD where it was appropriate, or I thought it was appropriate.
Much time has passed and now the compilers have fully supported the C ++ 11 standard. Since I am currently working in a company that develops computer games, a special requirement is placed specifically for development in C ++. Objective-C is alien to most employees. Yes, and I myself do not have a special love for this language (maybe only apart from its object model built according to the principles of the Smalltalk language).
Having read the specs according to the 11th standard, having studied a lot of bourgeois blogs, I decided to write my bike similar to the Apple CGD. Of course, I don’t set myself the goal of embracing the immaculate and limited myself to the implementation of the “Thread Pool” pattern and the opportunity to go out of the context of the secondary thread to the context of the main thread at any time, and vice versa.
')
For this, I needed the following C ++ 11 innovations - std :: function, variadic templates and of course working with std :: thread. (std :: shared_ptr is used only for a sense of self-comfort). Of course, another goal that I set for myself is cross-platform. And I was very disappointed when I learned that the compiler from Microsoft, staffed in VS 2012, did not support the variadic templates. But, after checking out a little stackoverflow, I saw that this problem was also solved by installing the additional package “Visual C ++ November 2012 CTP”.
Implementation
As I already mentioned, this thread is based on the “Thread Pool” pattern. During the design, two classes “gcdpp_t_task” were allocated to aggregate the actual task being performed and gcdpp_t_queue, the queue that accumulates tasks.
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); }; };
As we can see, this class is a template. And this creates a problem for us - how can we store tasks in one queue, if they are of different types?
Long ago I wondered why there is still no full-fledged implementation of interfaces / protocols in C ++. After all, the principle of programming from abstraction is more effective than from implementation. Well, nothing, you can create and abstrakny class.
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); }; };
Now, by inheriting our task class from a task abstraction, we can easily put everything in one queue.
Let's stop a bit and look at the gcdpp_t_task class. As I already mentioned, the class is template. It accepts a function pointer (in the specific implementation of the lambda expression) and a set of parameters. It implements only one method execute, in which functions are passed to the locked parameters. That's where the headache began. How to block the parameters in such a way that they can be transmitted in a deferred call. The decision to use std :: tuple came to the rescue.
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); }
Well, it seems like everything became transparent and clear. Now it's up to you to organize the "Thread Pool" with priorities.
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); };
This is the actual interface that implements the aggregation and encapsulation of the task queue. In the constructor, each object of the gcdpp_t_queue class creates its own thread in which the assigned tasks will be executed. Naturally, operations such as push and pop are wrapped in a mutex sync object, for safe operation in a multithreaded environment. I also needed a class that implements a similar functionality, but it works exclusively in the main thread. gcdpp_t_main_queue is more modest in its content, as it is more trivial.
And now the most important thing is to arrange it all in a less working form.
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); };
The class gcdpp_impl is singleton and fully encapsulated from external influences. It contains an array of 3 task pools (with priorities, while priorities are implemented by stubs), and a pool for executing tasks on the main thread. The class also contains 5 friend functions. The functions gcdpp_dispatch_init_main_queue and gcdpp_dispatch_update_main_queue are parasites. Right now I am developing an ominous plan for cutting them out. gcdpp_dispatch_update_main_queue - functions for processing tasks on the main thread ... and really want to save the user from sticking this function into your Run Loop.
With the rest of the functions, everything seems to be transparent:
gcdpp_dispatch_get_global_queue — get priority queue;
gcdpp_dispatch_get_main_queue - get a queue on the main thread;
gcdpp_dispatch_async - puts the operation queue for deferred call in a specific thread in a specific queue.
Application
And why is all this necessary?
I will try to show the profit of this implementation on several tests:
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");
In this example, the function declared in lambda expression will be deferred in a stream with high priority.
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);
This is an example of using a deferred operation call with custom classes as a parameter.
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); }
And the most important test is the operation call on the main thread from the secondary thread. The _OnTemplateLoaded function is called from the background of the thread that parses the xml settings file. After that, a particle buffer should be created and the structures should be sent to VRAM. This operation requires performing exclusively on the stream in which the graphics context was created.
Conclusion
In general, the problem is solved within the set goals. Of course, there is still a lot of things that have not been worked out and have not been tested, but for the time being the spark in me will be burning, I will continue to improve my implementation of GCD. Approximately 18 hours of work was spent on this project, most of which was a work break.
Source codes can be found in the open
source source code . Under VS 2012, the project has not yet been pushing, but I think it will soon appear there.
PS Waiting for adequate criticism ...