2015-10-28 2 views
-1

Фабричные принадлежности для асинхронных задач Процессоры разных типов. Процессор не знает подробностей задач и выполняет их через известный интерфейс. Динамическое распределение запрещено по соображениям производительности. Фабрика не должна владеть задачами, потому что иначе процессор должен будет сообщить Factory, когда закончит выполнение задачи для очистки. Процессор должен знать только интерфейс, но не сами задачи. Процессор может владеть Задачами как непрозрачные объекты, пока он обрабатывает их.Собственность задачи в Factory-Processor model

Возможное решение: хранить все виды задач внутри объединения «Интерфейс & буфер заполнения». Пожалуйста, рассмотрим следующий рабочий пример (C++ 11):

#include <iostream> 

struct Interface 
{ 
    virtual void execute() {} 
}; 

union X 
{ 
    X() {} 
    Interface i; 
    char padding[1024]; 
    template <class T> 
    X& operator= (T &&y) 
    { 
     static_assert (sizeof(T) <= sizeof(padding), "X capacity is not enough!"); 
     new (padding) T(y); 
    } 
}; 

struct Task : public Interface 
{ 
    Task() : data(777) {} 
    virtual void execute() { std::cout << data << std::endl; } 
    int data; 
}; 

int main() 
{ 
    Task t; 
    X x; 
    x = std::move(t); 
    Interface *i = &x.i; 
    i->execute(); 
}; 

Сниппет хорошо работает (печатает 777). Но существуют ли какие-либо опасности (например, виртуальное наследование) в таком подходе? Может быть, возможно лучшее решение?

+0

См.: ['Std :: aligned_union' (ru.cppreference.com)] (http://en.cppreference.com/w/cpp/types/aligned_union). Он предназначен для использования вместе с размещением нового и явного вызова деструктора. – rwong

ответ

0

Обновленный ответ.

std::aligned_union (en.cppreference.com). Он предназначен для использования вместе с размещением нового и явного вызова деструктора.


Ниже ранее ответ, теперь убирается.


С точки зрения дизайна,

  • избежать динамического распределения кажется резкое требование. Это требует особого оправдания.
  • В случае, если по какой-либо причине никто не доверяет стандартному распределителю, все же можно реализовать собственный распределитель, чтобы иметь полный контроль над его поведением.
  • Если есть класс или метод, который «владеет» всеми экземплярами всего: все Заводы, все Процессоры и все Задачи (как в вашем методе main()), тогда нет необходимости копировать что-либо. Просто передайте ссылки или указатели, так как этот «класс или метод, который владеет всем» будет заботиться о жизни объекта.

Мой ответ применим только к вопросу о «тетсру».

Я не пытаюсь охватить проблему тетсра-тов между «Задачей, которая имеет интерфейс в качестве базового класса и X, который имеет интерфейс, как член». Это не кажется универсальным для всех компиляторов C++, но я не знаю, из каких компиляторов C++ выйдет этот код.


Короткий ответ, который применим для всех компиляторов C++:

В настоящее время тривиальной Copyable списков «нет виртуальных функций» в качестве одного из необходимых условий, так что «в соответствии со спецификацией» Ответ в том, что ваша структура Task не тривиальный копируемая.


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

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


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

struct Task : public Interface 
{ 
    Task(std::string&& s) 
     : data(std::move(s)) 
    {} 
    virtual void execute() { std::cout << data << std::endl; } 
    std::string data; 
}; 

Причины этого является проблематичным является то, что при достаточно длинными строками, std::string будет выделять динамическую память для хранения ее содержимого. Если есть два экземпляра Task, а memcpy используется для копирования его байтов из одного экземпляра в другой экземпляр (который бы скопировал по внутренним полям класса std::string), их указатели укажут на один и тот же адрес, и, следовательно, их деструкторы оба попытаются удалить одну и ту же память, что приведет к неопределенному поведению. Кроме того, если экземпляр, который был перезаписан, имел более раннее строковое значение, память не будет освобождена.

Поскольку вы сказали, что «динамическое распределение запрещено», я предполагаю, что вы не будете использовать std::string или что-нибудь подобное, вместо этого предпочитаете писать только C-код. Поэтому эта озабоченность может не иметь отношения к вам.


Говоря о «низком уровне C-подобный код», вот моя идея:

struct TaskBuffer 
{ 
    typedef void (*ExecuteFunc) (TaskBuffer*); 
    ExecuteFunc executeFunc; 
    char padding[1024]; 
}; 

void ProcessMethod(TaskBuffer* tb) 
{ 
    (tb->executeFunc)(tb); 
} 
+0

На самом деле, нет необходимости, чтобы объект был тривиально скопируемым, чтобы скопировать его смежно (наконец, std :: vector может их хранить). Потому что мелкой копии достаточно и предпочтительнее! Проблема моя возникает, если rvalue 'y' уничтожит данные. Я проверил приведенный выше пример, деструктор вызывается только на 't', но не' y' (конечно, 'y' является ссылкой, но должно быть соответствующее временное)! Чтобы обеспечить безопасность данных, 'y' может быть сорван с момента получения данных после того, как будет выполнена« memcpy ». – midenok

+0

@midenok в вашем примере кода, он не использует memcpy для копирования между двумя экземплярами 'Task'. Он копирует из одной задачи в массив байтов. Функция, которая выполняет это копирование, практически ничего не знает о 'y', поэтому он не будет вызывать деструктор' y'. В вашем эксперименте деструктор 't' фактически вызывается в конце метода (main) (main)' main() ', который является примером уничтожения объектов, связанных с областью. – rwong

+0

Конечно, 'y' является ссылкой, и деструктор не называется ссылкой. Но 'std :: move' должен был создать ** временную ** ... F.ex., семантику копирования (подпись' T y' вместо 'T && y') приводит к' y. ~ Task() ' вызов. Это должно быть мое отсутствие знаний о семантике перемещения ... Итак, если семантика перемещения была разработана, чтобы не разрушить временную, тогда все хорошо, как есть! – midenok

0

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

Я хотел бы использовать устройство, как это:

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

  • один, чтобы вернуть указатель на доступный в данный момент буфер
  • один для передачи задания

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

Отправка новой задачи теперь делается так:

void * buffer = processor.getBuffer(); 
Task * task = new (buffer) Task(buffer); 
processor.submitJob(task); 

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

+0

Просто замените memcpy() с размещением new. Выглядит лучше, спасибо вам! Но дальнейшие предложения не так совершенны: для пула памяти требуется дополнительная логика (внешнее обслуживание, защита от параллелизма), что слишком медленнее, чем просто копирование задачи (это маленький объект, а в идеале - только один дескриптор файла). – midenok

+0

@midenok (1) Пул памяти не обязательно реализуется с использованием фрилансера. Растровое изображение также будет работать. (2) Пул памяти может быть спроектирован как локальный поток. – rwong

+0

@rwong (1) Тогда растровое изображение является «freelist». Здесь нет смысла обсуждать, как это можно реализовать. (2) TLS - это «защита от параллелизма», а также медленнее обычной памяти. Независимо от того, какая реализация, результат будет намного медленнее. (3) Сколько должно быть пул? Если это превысит, что тогда? Это создает множество дополнительных осложнений (что приведет к проблемам и дополнительному обслуживанию), где мы можем просто работать над стеком. – midenok

Смежные вопросы