2016-06-13 2 views
3

Я реализую свою собственную очередь, которая блокируется на .pop(). Эта функция также принимает дополнительный аргумент, который является таймаутом. Так что на данный момент у меня есть такой код:Исключение исключения с кодом возврата

template <class T> 
class BlockingQueue { 

private: 
    std::queue<T> m_queue; 
    std::mutex m_mutex; 
    std::condition_variable m_condition; 

public: 
    T pop(uint64_t t_millis) { 
     std::unique_lock<std::mutex> lock(m_mutex); 
     auto status = m_condition.wait_for(
      lock, 
      std::chrono::milliseconds(t_millis), 
      [=] { 
       return !m_queue.empty(); 
      } 
     ); 
     if (!status) { 
      throw exceptions::Timeout(); 
     } 
     T next(std::move(m_queue.front())); 
     m_queue.pop(); 
     return next; 
    }; 
} 

где exceptions::Timeout мой обычай исключение. Теперь я думал об этом исключении, бросая с точки зрения производительности. Было бы лучше вернуть какой-то код возврата из этой функции? Как это влияет на производительность?

Также, поскольку .pop уже возвращает что-то, как реализовать дополнительный код возврата? Я полагаю, что потребуется новая структура, которая содержит как T, так и код возврата. Неужели это увеличение сложности действительно стоит того?

+0

Является ли время исключительным случаем? Если да, бросьте исключение. Если нет, то реализуйте его по-разному. –

+1

[FYI] Вы не можете возвращать элемент очереди из 'pop': http://stackoverflow.com/questions/25035691/why-doesnt-stdqueuepop-return-value – NathanOliver

+1

' pop() ', который возвращает значение * и * выдает исключение? Хорошо, тогда я думаю. –

ответ

4

Выбросить исключения, если ожидания не были выполнены, вернуть код состояния, когда вы запрашиваете статус.

, например:

/// pops an object from the stack 
/// @returns an object of type T 
/// @pre there is an object on the stack 
/// @exception std::logic_error if precondition not met 
T pop(); 

/// queries how many objects are on the stack 
/// @returns a count of objects on the stack 
std::size_t object_count() const; 

/// Queries the thing for the last transport error 
/// @returns the most recent error or an empty error_code 
std::error_code last_error() const; 

и затем есть маршрут ASIO стиле реактора в сочетании с ИСПОЛНИТЕЛЕМ на основе фьючерсов:

/// Asynchronously wait for an event to be available on the stack. 
/// The handler will be called exactly once. 
/// to cancel the wait, call the cancel() method 
/// @param handler is the handler to call either on error or when 
///  an item is available 
/// @note Handler has the call signature void(const error_code&, T) 
/// 
template<class Handler> 
auto async_pop(Handler handler); 

, который можно было бы назвать так:

queue.async_pop(asio::use_future).then([](auto& f) { 
    try { 
    auto thing = f.get(); 
    // use the thing we just popped 
    } 
    catch(const system_error& e) { 
    // e.code() indicates why the pop failed 
    } 
}); 
+2

Если это очередь с одним потребителем, тогда 'logic_error' подходит для ее вызова при пустом. Однако, если это многопользовательская поточно-безопасная очередь, то у вызывающего нет средств гарантировать, что очередь не пуста (TOCTOU), и в этом случае она должна, вероятно, выбросить что-то другое. –

+0

К вашему предлагаемому API по-прежнему нужна функция ожидания по времени. Учитывая то, что у вас есть, я бы предложил либо 'void wait_non_empty (uint64_t t_millis);' или 'std :: size_t wait_non_empty (uint64_t t_millis),' возвращающ 'object_count()'. Или живите опасно и просто называйте это 'wait' вместо' wait_non_empty'. Надеюсь, это очевидно, какое условие вы ждете в очереди! –

+0

@SteveJessop или спуститься по маршруту реактора asio. 'template auto async_pop (обработчик && обработчик);' –

0

Исключением здесь не является. «Тайм-аут» - это, как ожидалось, результат получения элемента из очереди. Без тайм-аут программа по сути эквивалентна проблеме остановки. Предположим, клиент указал, что им нужен неопределенный тайм-аут. Будет ли исключение когда-либо бросать? Как бы вы справиться с таким исключением (если вы все еще живы в этом постапокалиптическом сценарии?)

Вместо этого я нахожу эти два варианта дизайна более логичным (хотя они не единственные):

  • Блокировать до тех пор, пока товар не будет доступен. Создайте функцию с именем wait, которая выполняет опрос и возвращает false, если время истекает, или true, когда товар доступен. Остальная часть вашей функции pop() может оставаться неизменной.

  • Не блокировать. Вместо того, чтобы вернуть статус:

    • Если операция будет блокировать, возвращение «занято»
    • Если очередь пуста, возвращение «пустое»
    • В противном случае, вы можете «поп» и вернуть «успех»

Поскольку у вас есть семафор, эти варианты кажутся предпочтительнее функцию т.е. не ждет.

+0

«Предположим, что клиент указал, что им нужен неопределенный тайм-аут» - давайте не будем преувеличивать, предел составляет 584 миллиона лет. –

+0

@SteveJessop Звучит для корпоративного класса. Я думаю, что большинство людей сдались через 5 минут. –

+0

'Блокировать до тех пор, пока не будет доступен элемент. Это будет блокировать вызывающего абонента навсегда, я не смог бы его прервать. Рассмотрим это: у меня есть рабочий, который выталкивает элемент из очереди. Но другой поток может отключить этого рабочего. Поэтому я делаю это через флаг 'is_running', который нужно проверять время от времени. Это был «тайм-аут». Также исправьте меня, если я ошибаюсь, но не будут ли оба решения терпеть неудачу из-за TOCTOU в случае нескольких потребителей? – freakish

0

Также, поскольку .pop уже возвращает что-то, как бы вы реализовали дополнительный код ? Я полагаю, что потребуется новая структура, которая содержит как T, так и код возврата .

Выполнение этого подхода потребует дополнительных требований к типам, которые могут использоваться с вашим BlockingQueue: они должны быть конструктивными по умолчанию. Этого можно избежать, если pop() возвращает результат через std::unique_ptr (сигнализируя о тайм-ауте с nullptr), но это приведет к заметным накладным расходам.

Я не вижу недостатка в использовании исключений здесь. Если вы измеряете свои таймауты в миллисекундах, то обработка исключения в случае таймаута должна быть незначительной.

+0

Сравните выделение исключения с возвратом 'bool'. Оба они незначительны по отдельности, но разница будет проявляться в больших количествах. –

+0

Но эти «высокие» номера будут ничем по сравнению с огромными цифрами, исходящими от тайм-аутов. – Leon

+0

@ Leon: лучший случай с кодом, который использует блокировки, не является конфликтом/блокировкой, и в этом случае не имеет значения, на сколько времени установлены тайм-ауты, они на самом деле 0, и что-то большое по сравнению с 0.Худший случай заключается в том, что блокировки в значительной степени защищены, а приложение привязано к процессору, и в этом случае что-то сохраненное поможет. Я согласен с тем, что почти весь код на C++ не должен беспокоиться о стоимости исключений, но сравнение их с таймаутом не позволяет понять, почему: это должно быть потому, что они дешевы по сравнению с другой обработкой вашего приложения. –

0

Один из способов оповестить об ошибке в подобной ситуации, не выбрасывая исключения, - это использовать что-то вроде шаблона expected<T> от Andrei Alexandrescu.

Он дал nice talk об этом некоторое время назад. Идея состоит в том, что expected<T> либо содержит T, либо содержит объект кода исключения/ошибки, описывающий, почему T не может быть произведен.

Вы можете использовать его реализацию или легко адаптировать идею в своих целях. Например, вы можете легко построить такой класс поверх boost::variant<T, error_code>.

Это еще один стиль обработки ошибок, отличный от целочисленных кодов ошибок C и исключений C++. Использование типа варианта не подразумевает каких-либо дополнительных динамических распределений - такой код может быть эффективным и не требует большой сложности.

Это фактически довольно близко к тому, как обработка ошибок выполняется в Rust идиоматически. c.f. 23