4

I'm implementing my own queue which blocks on .pop(). This function also accepts additional argument which is a timeout. So at the moment I have such code:

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;
    };
}

where exceptions::Timeout is my custom exception. Now I've been thinking about this exception throwing from the performance point of view. Would it be better to return some kind of return code from that function? How does that affect performance?

Also since .pop already returns something how would you implement additional return code? I suppose some new structure that holds both T and a return code would be needed. Is that increase in complexity really worth it?

freakish
  • 54,167
  • 9
  • 132
  • 169
  • Is a timing out an exceptional case? If so, throw an exception. If not, then implement it differently. – Cody Gray - on strike Jun 13 '16 at 12:17
  • 1
    [FYI] You may not want to return the queue item from `pop`: http://stackoverflow.com/questions/25035691/why-doesnt-stdqueuepop-return-value – NathanOliver Jun 13 '16 at 12:18
  • 1
    A `pop()` that returns a value *and* throws an exception? OK, then I guess. – uh oh somebody needs a pupper Jun 13 '16 at 12:18
  • Keep exceptions for exceptional things, use return values for normal flow. – Some programmer dude Jun 13 '16 at 12:19
  • @NathanOliver Googling "blocking queue C++" shows a lot of implementations where `pop()` returns a value. Throwing exceptions on the other hand: no. – uh oh somebody needs a pupper Jun 13 '16 at 12:23
  • I would lean towards if you expect the queue to time-out regularly as part of its function then return error, if it should not time-out unless something went wrong then exception. – Galik Jun 13 '16 at 12:29
  • @sleeptightpupper None of the implementations I've seen actually implements the timeout version of `.pop`. What would you return on timeout? – freakish Jun 13 '16 at 12:31
  • 1
    @freakish I'd separate the functions into two bits: one that returns a `bool` called `try_whatever` (non-blocking), then your blocking `pop()` here. I don't think an exception is necessary here. – uh oh somebody needs a pupper Jun 13 '16 at 12:36
  • 1
    "How would you implement return code?" one way would be something like this: https://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C You can implement an `expected` template class, which represents "either, successful construction of a value, or the error that prevented its creation" using e.g. `boost::variant` if you want. It's just another style of error handling, it's efficient and doesn't add much complexity, although it might not mesh well with your existing code – Chris Beck Jun 13 '16 at 12:37
  • 1
    "I've been thinking about this exception throwing from the performance point of view" -- that's a mistake. If you want to know about performance you need to measure it, not think about it. Quality of implementation can easily make an order-of-magnitude difference to the time taken to throw and catch an exception (or for that matter the time to acquire the lock in the successful case). – Steve Jessop Jun 13 '16 at 12:38
  • 1
    could always return a `boost::optional` as well - rather than exception.. – Nim Jun 13 '16 at 12:59
  • @ChrisBeck I really enjoyed that video and I think I'm going to go down that road. If you post it as an answer then I will accept it. – freakish Jun 14 '16 at 08:42
  • @NathanOliver Right, in practice `T` will be a pointer or a known type so copy constructor throwing an error is not an issue. – freakish Jun 14 '16 at 10:28
  • @SteveJessop As you can see the timeout is measured in milliseconds so I doubt I will be able to properly test it. Probably it won't matter at all. I might test both solutions in some other scenario though. Also thinking about performance is not a mistake at all. While I agree that tests are very important you still want to hear other people opinions in case you don't fully understand what's going on. And I do have to admit that I still feel like a junior in C++. – freakish Jun 14 '16 at 10:35

4 Answers4

4

Throw exceptions when an expectation has not been met, return a status code when you're querying for status.

for example:

/// 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;

and then there's the asio-style reactor route coupled with executor-based futures:

/// 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);

which could be called like this:

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
  }
});
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 2
    If this is a single-consumer queue then `logic_error` is appropriate for calling it when empty. However, if it's a multi-consumer thread-safe queue then the caller has no means to guarantee that the queue is non-empty (TOCTOU), in which case it should probably throw something different. – Steve Jessop Jun 13 '16 at 12:45
  • Btw your proposed API still needs a timed wait function. Given what you have I'd suggest either `void wait_non_empty(uint64_t t_millis);` or `std::size_t wait_non_empty(uint64_t t_millis);` returning the `object_count()`. Or live dangerously and just call it `wait` instead of `wait_non_empty`. Hopefully it's obvious what condition you wait for on a queue! – Steve Jessop Jun 13 '16 at 12:52
  • @SteveJessop or go down the asio reactor route. `template auto async_pop(Handler&& handler);` – Richard Hodges Jun 13 '16 at 13:57
1

Also since .pop already returns something how would you implement additional return code? I suppose some new structure that holds both T and a return code would be needed.

Going with this approach would put an extra requirement on the types that can be used with your BlockingQueue: they must be default constructible. It can be avoided if pop() returns the result through a std::unique_ptr (signaling the timeout with a nullptr), but that will introduce noticeable overhead.

I see no disadvantage of using exceptions here. If you are measuring your timeouts in milliseconds, then handling an exception in case of a timeout should be negligible.

Leon
  • 31,443
  • 4
  • 72
  • 97
  • Compare throwing an exception to returning a `bool`. Both are negligible individually, but the difference will show up in high numbers. – uh oh somebody needs a pupper Jun 13 '16 at 13:03
  • But those "high" numbers will be nothing compared to the enormous numbers coming from timeouts. – Leon Jun 13 '16 at 13:06
  • @Leon: the best case with code that uses locks is no contention/blocking, in which case it doesn't matter how long the timeouts are set to, they're actually 0 and anything is big compared with 0. The worst case is that the locks are heavily contended and the app is CPU-bound, in which case anything saved will help. I agree that almost all C++ code needn't worry about the cost of exceptions, but comparing them against the timeout doesn't capture why: it has to be because they're cheap compared with other processing your app does. – Steve Jessop Jun 13 '16 at 13:11
  • Or to put it another way: suppose your timeout (on a socket, perhaps) is 900 seconds which is a typical TCP-level timeout. The server might actually respond much faster than that almost always, so assessing performance as "anything less than 15 minutes is irrelevant for internet apps" is incorrect. In the worst case, sure you have 15 minutes to get your house in order, but you might need to be more responsive than that in the common case. – Steve Jessop Jun 13 '16 at 13:14
  • @SteveJessop I think my reasoning is not fully understood. I am saying that if you could afford to wait for something for 15 minutes, then if it doesn't arrive after 15 minutes (no matter if that happens 1% or 99% of the calls) you can afford spending several seconds on the handling of that situation and shouldn't strive to be able to do it in a nanosecond. – Leon Jun 13 '16 at 13:24
  • Note that I am not saying that you can also be slow when the data arrives in time. On the contrary, being able to process incoming data quickly is crucial for high throughput, however being sluggish after having waited for too long doesn't hurt performance significantly. – Leon Jun 13 '16 at 13:33
  • Back in my socket example: you don't want your browser to hog the CPU on every timeout, for a period of time even remotely comparable to the network timeout, which is an eternity, because there might be multiple threads waiting and the failed ones ruin the life of the successful ones. So assuming multiple consumers (or just other stuff going on), in this question we need that exceptions are cheap compared with whatever the app does, or else a timeout is a performance hit regardless of how long the thread spent doing nothing prior to timing out. Fortunately exceptions are cheap for most apps. – Steve Jessop Jun 13 '16 at 13:41
  • Agree. If CPU time and wall time are different (and completely independent) types of resources, then my argument doesn't stand the critique. Then it's like ordering something and then paying money (CPU time) when it arrives or doesn't arrive. I'd definitely be reluctant to pay a higher monetary price if my order doesn't arrive after an extended period of waiting (even though time is money!). – Leon Jun 13 '16 at 13:50
1

One way to signal an error in a situation like this, without throwing an exception, would be to use something like Andrei Alexandrescu's expected<T> template.

He gave a nice talk about it a while back. The idea is, expected<T> either contains a T, or it contains an exception / error code object describing why the T couldn't be produced.

You can use his implementation, or easily adapt the idea for your own purposes. For instance you can build such a class on top of boost::variant<T, error_code> quite easily.

This is just another style of error handling, distinct from C-style integer error codes and C++ exceptions. Using a variant type does not imply any extra dynamic allocations -- such code can be efficient and doesn't add much complexity.

This is actually pretty close to how error handling is done in Rust idiomatically. c.f. 2 3

Chris Beck
  • 15,614
  • 4
  • 51
  • 87
0

An exception is not necessary here. A "timeout" is just as expected an outcome as getting an item from the queue. Without the timeout, the program is essentially equivalent to the halting problem. Let's say the client specified that they want an indefinite timeout. Would the exception ever throw? How would you handle such an exception (assuming you're still alive in this post-apocalyptic scenario?)

Instead I find these two design choices more logical (though they're not the only ones):

  • Block until an item is available. Create a function named wait that polls and returns false if it times out, or true when an item is available. The rest of your pop() function can remain unchanged.

  • Don't block. Instead return a status:

    • If the operation would block, return "busy"
    • If the queue is empty, return "empty"
    • Otherwise, you can "pop" and return "success"

Since you have a mutex, these options seem preferable to an i.e. non-waiting function.

  • "Let's say the client specified that they want an indefinite timeout" - let's not exaggerate, the limit appears to be 584 million years – Steve Jessop Jun 13 '16 at 12:58
  • @SteveJessop Sounds enterprise-grade to me. I think most people would give up after 5 minutes. – uh oh somebody needs a pupper Jun 13 '16 at 13:01
  • `Block until an item is available` That would basically block the caller forever, I wouldn't be able to interrupt it. Consider this: I have a worker which pops an element from a queue. But some other thread might disable that worker. So I do it via `is_running` flag which has to be checked every now and then. This is were `timeout` comes in play. Also correct me if I'm wrong but won't both solutions fail due to TOCTOU in case of multiple consumers? – freakish Jun 13 '16 at 13:02
  • @freakish `Block the caller forever` On that current thread yes. Not sure how that's different from your current code. `.. won't both solutions fail due to TOCTOU ..` Outside the scope of the question. See [C++0x has no semaphores? How to synchronize threads?](https://stackoverflow.com/questions/4792449/c0x-has-no-semaphores-how-to-synchronize-threads) – uh oh somebody needs a pupper Jun 13 '16 at 13:14
  • @sleeptightpupper: the current code does not block the thread forever, it only blocks it for `t_millis` milliseconds. If you're saying "never implement timed wait" then I think you're missing an important requirement of the question. But actually I think freakish has read "Block until an item is available" but ignored how in the next sentence you say, "returns false if it times out". So it doesn't block forever unless the caller asks it to – Steve Jessop Jun 13 '16 at 13:17
  • @sleeptightpupper `Block the caller forever` Well, due to timeout it only blocks for a given time (some millis) after which I can check the `is_running` flag. My `.pop` function is called in a `while (is_running)` loop. So in your case if the queue is empty I wouldn't be able to break the loop ever. As for `TOCTOU`: it kind of is. The whole reason for putting both `queue.pop` and `queue.front` in one function is to have it all wrapped in a lock which guarantees that TOCTOU is never an issue. – freakish Jun 13 '16 at 13:18
  • @SteveJessop Sounds like I misphrased it. Block until it times out *or* an item is available. – uh oh somebody needs a pupper Jun 13 '16 at 13:19
  • @freakish: Agreed. If you have multiple consumers then TOCTOU is still an issue, and even if you implement a timed wait function described here, you still *also* need to decide what `pop()` does when the queue is empty. Which it might be, even if the caller has first waited successfully. – Steve Jessop Jun 13 '16 at 13:21