Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Thread concurrency C 11, свой велосипед спецтехнологии (Apple) GCD

Anna | 25.06.2014 | нет комментариев

Добросердечный вечер прогровчане. В данной статье хочу описать задачи работы в многопоточной среде, с которыми я встретился и пути их решения. Больше пяти лет я занимаюсь разработкой игровых планов на С / Objective C , в оснвоном под платформу iOS. 2 года назад решил испробовать себя в «нативной» разработке применяя только Objective-C. Приблизительно в тоже время меня заинтересовала спецтехнология GCD от Apple (как раз позже просмотра очередного WWDC). В первую очередь, в этой спецтехнологии меня привлекла эластичная вероятность делегирования операций между потоками. Достаточно распространненой задачей является загрузка каких-либо игровых источников в низкоприоритетном потоке. Но достаточно нетривиальной задачей является смена потока по окончанию операции загрузки на основной поток с целью последующей загрузки в VRAM. Безусловно дозволено было закрыть глаза на эту загвоздку и применять Shared Context для графического контекста, но ростущий в то время во мне перфикционизм к собственному коду и решениям проектирования графических систем, не дозволил поступить так. В всеобщем было принято решение опробовать GCD на «пет» плане, которым я как раз в то время занимался. И получилось достаточно не нехорошо. Помимо задач решающих загрузку игровых источников я стал применять GCD там где это было целесообразно, ну либо мне казалось, что это было целесообразно.

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

Почитав спеки по 11 эталону, проштудировав уйма буржуинских блогов я решился написать свой велосипед схожий с Apple CGD. Безусловно я не ставлю себе за цель обьять необьятное и ограничился лишь реализацией паттерна «Пул потоков» и вероятностью выйти в всякий момент из контекста второстепенного потока на контекст основного потока, и напротив.

Для этого мне потребовались следующие новшевства С 11 — std::function, variadic templates и безусловно работы с std::thread. (std::shared_ptr применяется лишь для чувства собственного успокоения). Безусловно еще одна цель, которую я поставил перед собой — это кроссплатформенность. И дюже был разочарован, когда узнал, что компилятор от Microsoft, укомплектованый в VS 2012, не поддерживал variadic templates. Но, поштудировав немножко stackoverflow, я увидел, что и эта задача решается установкой допольнительного пакета «Visual C November 2012 CTP».

Реализация

Как я теснее упоминал, в основе этой идеи лежит паттерн «Пул потоков». При проектировании было выделено два класса «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);
    };
};

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

Давным-давным-давно задаюсь вопросом, отчего до сих пор в С нет полновесной реализации интерфейсов/протоколов. чай правило программирования от абстракции больше результативен, чем от реализации. Ну ничего, дозволено сделать и абстракный класс.

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. Как я теснее упоминал, класс является шаблонным. Принимает он указатель на функцию (в определенной реализации представленной лямбда выражением) и комплект параметров. Реализует лишь один способ execute, в котором функции передаются засторенные параметры. Вот здесь как раз и началась головная боль. Как же засторить параметры в таком виде, Дабы дозволено было в последующем их передать в отложенном вызове. На поддержка пришло решение применять 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 обернуты в обьект синхогизации mutex, для безвредной работы в многопоточной среде. Также мне потребовался класс, реализующий схожий функционал, но работающий экстраординарно в основном потоке. 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 friend функции. Функции 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. Данная операция требует выполнения экстраординарно на том потоке, в котором был сделан графический контекст.

Завершение

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

Начальные коды дозволено обнаружить в открытом доступе source code. Под VS 2012 план пока не пушил, но думаю в скором времени он там появится.

P.S. В ожидании адекватной критики…

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB