TooManyCooks
TooManyCooks copied to clipboard
`channel::try_pull()`
Currently data can only be read from a channel using std::optional<T> data = co_await chan.pull();. It would be useful to have a try_pull function which would simply return immediately rather than block when data is not available.
Possible APIs:
- the function could result in 3 possibilities: success, failure because queue was empty, or failure because queue was closed. Thus, a boolean or optional-returning API is insufficient. Returning a
chan_errorenum could solve this issue. - Returning a std::expected would be nice for this; however that requires C++23. Should I increase the required compiler level, or use another implementation? Both https://github.com/TartanLlama/expected and https://github.com/martinmoene/expected-lite are available as single-header with compatible licenses.
- Another option would be to return a std::variant.
- Or a std::tuple which always contains both the value and an error code. With tuple-destructuring syntax
auto [data, err] = chan.try_pull();this is quite similar to Go / asio error handling, and is (IMO) less painful than checking and handling the variant types. - A final option would be to return an error code, and require the user to pass an object of the result type by reference, which then has the queue data move-constructed into it.
T data; auto err = chan.try_pull(data);This is an old-school pattern that has several downsides. 1. Requires 2 statements/lines to separately declare the object and pass it (this explicitly goes against the design philosophy of TMC, which avoids this pattern in every other case). 2. Requires the object be default-constructible. 3. May introduce additional overhead by calling the default constructor before calling the move-constructor.
Implementation notes:
pull()is implemented using a speculativefetch_add; it always gets the ticket first and then afterward finds the block/data element and determines if data is ready. If data is not ready, then it suspends itself and stores its handle into that element.try_pull()could instead load the write and read indexes, and then try to perform acompare_exchangeon the read index only if there appears to be data available (the read index is behind the write index). If this CAS succeeds, then it's guaranteed to be able to read the data. However, retries could be expensive in the case on high contention.- I think
try_pull()could also use the speculativefetch_addwith careful handling:- If data is ready, no problem, just return the data.
- If data is not ready, rather than suspending, mark the element as "done"; this operation needs to be a CAS.
- If the CAS succeeds, then return false / empty.
- If the CAS fails, then the data is now ready, so return the data.
- However, this requires writers to handle a "done" element in their slot and retry to get a new write ticket. This handling may be a non-zero-cost operation... thus making try_pull faster at the cost of overall (write) performance.
- Additional note: currently, writers load the value of flags and check for the presence of a waiting reader before attempting a CAS. Perhaps this could be optimized by simply attempting the CAS immediately. The impact of this needs to be benchmarked.