-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[New Feature] Timeout-capable channels #51
base: master
Are you sure you want to change the base?
Conversation
Edit: removed New benchmarks without hand tuning show slight reduction in overhead in the
|
I'm giving this feature a chance. I will review it after all the jobs have been passed. |
Tried debugging the supposed race condition but it is hard to detect exactly where the double lock is happening. For Clang and MSVC there is no problem, and this bug report https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101978 suggests that in our situation ( Any chance that the CI/CD could be upgraded to use a more modern version of GCC to check if it's a compiler problem? (that would be the first I witness). edit: local compilation on my machine (Arch 6.12.8-zen1-1.1-zen, GCC 14.2.1) shows no errors, so a compiler bug in CI/CD is not unlikely. |
You can upgrade the jobs to Ubuntu 24.04. This version might have GCC 14. |
I will review this soon. |
sudo apt-get update | ||
sudo apt-get install -y gcc-12 g++-12 cmake |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"gcc-12 is already the newest version (12.3.0-1ubuntu1~22.04)."
Is update-alternatives
enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By removing the apt-get
call I get:
| update-alternatives: error: alternative path /usr/bin/gcc-12 doesn't exist
[build/Ubuntu 22.04 GCC (Debug)-2 ] ❌ Failure - Main Install GCC 12
I am honestly not familiar with Ubuntu so maybe I am not doing this correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you tried this locally, maybe you don't have GCC 12 already installed. On the GitHub runner, I see 12 is already installed, but not used as default. So just try it here without apt-get update and install.
friend class blocking_iterator<channel>; | ||
}; | ||
|
||
} // namespace msd | ||
|
||
#include "channel.inl" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't resolve conversations. They help to see if we understand one another. The author of the conversation resolves it when the changes are done accordingly.
In this specific case, #include "channel.inl"
is needed.
* @param timeout Duration after which operations will time out. | ||
*/ | ||
template <typename Rep, typename Period> | ||
void setTimeout(const std::chrono::duration<Rep, Period>& timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a really useful scenario where you want to set a timeout after the channel has been declared, clear the timeout, or change it?
Or would it be enough to have an overloaded constructor that accepts the capacity and the timeout? And then timeout_
does not need to be atomic. Simplicity is usually better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, this is probably over-engineered since most of the times you do not really need that to change at runtime/past the program's initialization.
The thing is that when I initially programmed this I tried to add time-out functionality on top of the original implementation as much as possible, so I was reluctant to touch the constructors, this is a good idea however and I'll refactor the PR as soon as I have time.
} | ||
|
||
return cnd_.wait_for(lock, timeout, pred); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to verify for if timeout_ is zero()? How about the following?
template <typename T>
template <typename Predicate>
bool channel<T>::waitWithTimeout(std::unique_lock<std::mutex>& lock, Predicate pred)
{
auto timeout = timeout_.load(std::memory_order_relaxed);
return cnd_.wait_for(lock, timeout, pred);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From cppreference:
wait_for causes the current thread to block until the condition variable is notified, the given duration has been elapsed, or a spurious wakeup occurs. pred can be optionally provided to detect spurious wakeup.
rel_time - the maximum duration to wait
Which means that if timeout == 0
(so our channel does not have a timeout, if we go with our proposed timeout-at-construction semantics), wait_for
will never wait for the predicate and will instantly timeout (problem!).
I think that explicit detection of timeout == 0 is necessary as this is not really a timeout of 0s, but a way for us to have a null timeout without:
- Having access to std::optional (C++17 >= only)
- Using
nullptr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will have another look at this.
@@ -67,15 +103,15 @@ void channel<T>::close() noexcept | |||
{ | |||
{ | |||
std::unique_lock<std::mutex> lock{mtx_}; | |||
is_closed_.store(true); | |||
is_closed_.store(true, std::memory_order_relaxed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that we do not need explicit memory ordering guarantees (I suppose), so we can give this internal hint in the hope that the compiler can provide the user with more optimized code.
Suppose that in an unlikely scenario the user is handling std::atomic
variables close to a channel<T>::close()
call, then the compiler could possibly generate better code.
In the case where this is not unsafe, it's basically free to do and it doesn't introduce significant complexity for the user (it is buried in the lib's code).
Commits according to @andreiavrammsd code review Co-authored-by: Andrei Avram <[email protected]>
Ran clang format on every file. Fixed typos and redundant functions.
@@ -29,7 +30,6 @@ see [CMakeLists.txt](./examples/cmake-project/CMakeLists.txt) from the [CMake pr | |||
|
|||
```c++ | |||
#include <cassert> | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert.
@@ -83,7 +83,6 @@ int main() { | |||
|
|||
```c++ | |||
#include <iostream> | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert.
friend class blocking_iterator<channel>; | ||
}; | ||
|
||
} // namespace msd | ||
|
||
#include "channel.inl" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't resolve conversations. They help to see if we understand one another. The author of the conversation resolves it when the changes are done accordingly.
In this specific case, #include "channel.inl"
is needed.
if (!ch.empty()) { | ||
out = std::move(ch.queue_.front()); | ||
ch.queue_.pop(); | ||
--ch.size_; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert.
ch.cnd_.notify_one(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert.
} | ||
|
||
return cnd_.wait_for(lock, timeout, pred); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will have another look at this.
|
||
|
||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary.
|
||
return 0; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary.
Features
I tried implementing my spin on #50 in the library while keeping API compatibility.
A timeout can be set on channel
ch
like this:ch.setTimeout(std::chrono::milliseconds(100));
Or, if the user is using >=C++14, in the more convenient way:
This applies a timeout on all
operator>>,operator<<
performed on the channel by using the already-existing condition variable member. if the timeout is exceeded, achannel_timeout
exception is thrown.The timeout can be removed by calling
Which is just a convenient alias for
ch.setTimeout(std::chrono::nanoseconds::zero())
The
timeout_
member is wrapped in astd::atomic
variable to prevent hazards during multi-threaded execution in a (hopefully) lock-free mannerCompatibility with existing code
The new header library runs all previous tests and examples successfully. New tests have been added to the
channel_test.cpp
suite to cover the new timeout functionality..Branching on whether a timeout is present or not always costs a bit of overhead on every operation as shown in the Benchmarks section.
Even if the library maintains 100% compatibility with previous versions, the feature provided here is not a zero-cost abstraction, so it is up to the maintainers to decide if its inclusion in the main repo is worth it.
Benchmarks
The timeout code adds a 7ns fixed overhead to operations when no timeout is being used, for a performance pessimisation of around ~20% (benchmarks below run on my i5-6500 with freq scaling off, release build).