Я ищу несколько советов по вопросам дизайна C ++, которые у меня есть. Некоторая история вопроса …
У меня есть класс Runnable, как показано ниже:
class Runnable
{
public:
Runnable();
virtual ~Runnable();
void Stop();
void Start();
Runnable(Runnable const&) = delete;
Runnable& operator =(Runnable const&) = delete;
protected:
virtual void Run() = 0;
// main thread function.
std::atomic<bool> mStop;
private:
static void StaticRun(void *);
std::thread mThread;
};
Затем у меня есть ExpirationMap, который наследует класс Runnable, как показано ниже:
class ExpirationMap : Runnable
{
public:
explicit ExpirationMap();
virtual ~ExpirationMap();
void Init(uint8_t);
void Run() override;
virtual void DoExpire(uint8_t) = 0;
// Expiry function to be implemented by the derived classes.
private:
uint8_t mDelay;
};
У меня есть третий класс, который наследует класс ExpirationMap. Этот класс инкапсулирует std :: unorderd_map.
шаблон
class MyMap : public ExpirationMap
{
public:
void DoExpire(uint8_t) override;
void Init(uint8_t);
void Add(const KeyType, const ValueType&);
ValueType Get(const KeyType);
bool Exists(const KeyType);
ValueType Remove(const KeyType);
void Clear();
...
private:
std::unordered_map<KeyType, ValueType> mMap;
std::shared_ptr<boost::shared_mutex> mLock;
};
MyMap :: Init запускает ExpirationMap :: Init, который порождает поток с MyMap :: DoExpire в качестве функции потока. MyMap :: DoExpire — это, по сути, бесконечный цикл while. Основная задача потока — сканировать элементы MyMap и удалять просроченные записи. Каждый элемент (значение) карты имеет время истечения, которое используется, чтобы проверить, является ли элемент кандидатом на истечение срока действия. Все это реализовано и работает хорошо.
Извините за длинное вступление, но теперь перейдем к реальной проблеме.
Теперь у меня есть ситуация, когда я должен перенести этот код на платформу, основанную на цикле событий. Поскольку система обработки событий поддерживает таймеры с обратными вызовами, я мог бы передать функцию DoExpire в качестве обратного вызова для функции таймера. Тем не менее, я пытаюсь выяснить, существует ли лучший способ реорганизовать код, чтобы код работал как на платформах, т.е. на основе потоков (то, что у меня есть сейчас), так и на основе циклов обработки событий при минимизации дублирования. При создании MyMap я хочу сказать: создайте карту, которая использует истечение на основе потока или истечение на основе таймера + обратного вызова. Любые предложения или советы с благодарностью. Благодарю.
Я думаю, что вы можете сделать лучше, чем любой другой подход — вы можете сделать это так, чтобы вам вообще не нужно было периодически что-либо делать, и, таким образом, вам не понадобятся ни циклы событий, ни поток обновления.
Поскольку с каждой записью в вашей карте уже связано время истечения, все, что вам нужно сделать, это построить слой API вокруг объекта карты, который притворяется, что объект с истекшим сроком действия больше не существует, например, (Псевдокод):
bool ExpirationMap :: Exists(const KeyType & key) const
{
if (mMap.has_key(key) == false) return false;
return (mMap[key].mExpirationTime < now); // expired entries don't count!
}
ValueType ExpirationMap :: Get(const KeyType & key) const
{
return Exists(key) ? mMap[key] : ValueType();
}
Этого достаточно, чтобы получить желаемое поведение; единственная остающаяся проблема (которая может или не может быть реальной проблемой в зависимости от вашего варианта использования) заключается в том, что со временем карта может стать большой, заполненной бесполезными старыми / просроченными записями. Это может быть обработано различными способами (включая просто игнорирование проблемы, если использование памяти оказывается не проблемой, или удаление записи только тогда, когда она ищется и считается, что срок ее действия истек), но один способ, близкий к оптимальному обрабатывать его можно было бы, чтобы сохранить вторую внутреннюю структуру данных (например, std :: priority_queue, которая содержит записи, отсортированные по времени истечения); тогда каждый раз, когда вызывается любой метод, вы можете сделать что-то вроде:
while(mEntriesByExpirationTime.size() > 0)
{
const ByTimeEntry & firstEntry = mEntriesByExpirationTime.begin();
if (firstEntry.mExpirationTime < now)
{
mMap.erase(firstEntry.mKey);
mEntriesByExpirationTime.pop();
}
else break;
}
… Поскольку записи в этом priority_queue хранятся в порядке истечения времени, этот вызов является настолько дешевым, насколько это возможно, поскольку он никогда не будет повторяться больше, чем просто с просроченными записями, которые должны быть удалены прямо сейчас.
Конструкция, которая не требует, чтобы ваша программа просыпалась через регулярные промежутки времени, как правило, предпочтительнее, чем та, которая требуется, особенно на платформах с ограниченным энергопотреблением, таких как ноутбуки и телефоны. Процессор не может спать эффективно, если ваша программа требует, чтобы ее периодически просыпали 🙂
Других решений пока нет …