Skip to content

Commit b9b46c5

Browse files
jmachowinskiJanosch Machowinski
andauthored
feat: Add ClockWaiter and ClockConditionalVariable (#2691)
* feat: Add ClockWaiter and ClockConditionalVariable Added two new synchronization primitives to perform waits using an rclcpp::Clock. Signed-off-by: Janosch Machowinski <[email protected]> * fix: Deprecate broken API Signed-off-by: Janosch Machowinski <[email protected]> --------- Signed-off-by: Janosch Machowinski <[email protected]> Co-authored-by: Janosch Machowinski <[email protected]>
1 parent 30e61c9 commit b9b46c5

File tree

5 files changed

+615
-1
lines changed

5 files changed

+615
-1
lines changed

rclcpp/include/rclcpp/clock.hpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,17 @@ class Clock
201201
ros_time_is_active();
202202

203203
/**
204+
* Deprecated. This API is broken, as there is no way to get a deep
205+
* copy of a clock. Therefore one can experience spurious wakeups triggered
206+
* by some other instance of a clock.
207+
*
204208
* Cancels an ongoing or future sleep operation of one thread.
205209
*
206210
* This function can be used by one thread, to wakeup another thread that is
207211
* blocked using any of the sleep_ or wait_ methods of this class.
208212
*/
209213
RCLCPP_PUBLIC
214+
[[deprecated("Use ClockConditionalVariable")]]
210215
void
211216
cancel_sleep_or_wait();
212217

@@ -267,6 +272,117 @@ class Clock
267272
std::shared_ptr<Impl> impl_;
268273
};
269274

275+
/**
276+
* A synchronization primitive, equal to std::conditional_variable,
277+
* that works with the rclcpp::Clock.
278+
*
279+
* For more information on the API see https://en.cppreference.com/w/cpp/thread/condition_variable.
280+
*
281+
* Note, this class does not handle shutdowns, if you want to
282+
* haven them handles as well, use ClockConditionalVariable.
283+
*/
284+
class ClockWaiter
285+
{
286+
private:
287+
class ClockWaiterImpl;
288+
std::unique_ptr<ClockWaiterImpl> impl_;
289+
290+
public:
291+
RCLCPP_SMART_PTR_DEFINITIONS(ClockWaiter)
292+
293+
RCLCPP_PUBLIC
294+
explicit ClockWaiter(const rclcpp::Clock::SharedPtr & clock);
295+
296+
RCLCPP_PUBLIC
297+
~ClockWaiter();
298+
299+
/**
300+
* Calling this function will block the current thread, until abs_time is reached,
301+
* or pred returns true.
302+
* @param lock A locked lock. The lock must be locked at call time, or this method will throw.
303+
* The lock will be atomically released and this thread will blocked.
304+
* @param abs_time The time until which this thread shall be blocked.
305+
* @param pred may be called in cased of spurious wakeups, but must be called every time
306+
* notify_one() was called. During the call to pred, the given lock will be locked.
307+
* This method will return, if pred returns true.
308+
*/
309+
RCLCPP_PUBLIC
310+
bool
311+
wait_until(
312+
std::unique_lock<std::mutex> & lock,
313+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred);
314+
315+
/**
316+
* Notify the blocked thread, that it should reevaluate the wakeup condition.
317+
* The given pred function in wait_until will be reevaluated and wait_until
318+
* will return if it evaluates to true.
319+
*/
320+
RCLCPP_PUBLIC
321+
void
322+
notify_one();
323+
};
324+
325+
326+
/**
327+
* A synchronization primitive, similar to std::conditional_variable,
328+
* that works with the rclcpp::Clock.
329+
*
330+
* For more information on the API see https://en.cppreference.com/w/cpp/thread/condition_variable.
331+
*
332+
* This primitive will wake up if the context was shut down.
333+
*/
334+
class ClockConditionalVariable
335+
{
336+
class Impl;
337+
std::unique_ptr<Impl> impl_;
338+
339+
public:
340+
RCLCPP_SMART_PTR_DEFINITIONS(ClockConditionalVariable)
341+
342+
RCLCPP_PUBLIC
343+
ClockConditionalVariable(
344+
const rclcpp::Clock::SharedPtr & clock,
345+
rclcpp::Context::SharedPtr context = rclcpp::contexts::get_global_default_context());
346+
RCLCPP_PUBLIC
347+
~ClockConditionalVariable();
348+
349+
/**
350+
* Calling this function will block the current thread, until abs_time is reached,
351+
* or pred returns true.
352+
* @param lock A locked lock. The lock must be locked at call time, or this method will throw.
353+
* The lock will be atomically released and this thread will blocked.
354+
* The given lock must be created using the mutex returned my mutex().
355+
* @param abs_time The time until which this thread shall be blocked.
356+
* @param pred may be called in cased of spurious wakeups, but must be called every time
357+
* notify_one() was called. During the call to pred, the given lock will be locked.
358+
* This method will return, if pred returns true.
359+
*
360+
* @return true if until was reached.
361+
*/
362+
RCLCPP_PUBLIC
363+
bool
364+
wait_until(
365+
std::unique_lock<std::mutex> & lock, rclcpp::Time until,
366+
const std::function<bool ()> & pred);
367+
368+
/**
369+
* Notify the blocked thread, that is should reevaluate the wakeup condition.
370+
* E.g. the given pred function in wait_until shall be reevaluated.
371+
*/
372+
RCLCPP_PUBLIC
373+
void
374+
notify_one();
375+
376+
/**
377+
* Returns the internal mutex. In order to be race free with the context shutdown,
378+
* this mutex must be used for the wait_until call.
379+
*/
380+
RCLCPP_PUBLIC
381+
std::mutex &
382+
mutex();
383+
};
384+
385+
270386
} // namespace rclcpp
271387

272388
#endif // RCLCPP__CLOCK_HPP_

rclcpp/src/rclcpp/clock.cpp

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,245 @@ Clock::create_jump_callback(
367367
// *INDENT-ON*
368368
}
369369

370+
class ClockWaiter::ClockWaiterImpl
371+
{
372+
private:
373+
std::condition_variable cv_;
374+
375+
rclcpp::Clock::SharedPtr clock_;
376+
bool time_source_changed_ = false;
377+
std::function<void(const rcl_time_jump_t &)> post_time_jump_callback;
378+
379+
bool
380+
wait_until_system_time(
381+
std::unique_lock<std::mutex> & lock,
382+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred)
383+
{
384+
auto system_time = std::chrono::system_clock::time_point(
385+
// Cast because system clock resolution is too big for nanoseconds on some systems
386+
std::chrono::duration_cast<std::chrono::system_clock::duration>(
387+
std::chrono::nanoseconds(abs_time.nanoseconds())));
388+
389+
return cv_.wait_until(lock, system_time, pred);
390+
}
391+
392+
bool
393+
wait_until_steady_time(
394+
std::unique_lock<std::mutex> & lock,
395+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred)
396+
{
397+
// Synchronize because RCL steady clock epoch might differ from chrono::steady_clock epoch
398+
const rclcpp::Time rcl_entry = clock_->now();
399+
const std::chrono::steady_clock::time_point chrono_entry = std::chrono::steady_clock::now();
400+
const rclcpp::Duration delta_t = abs_time - rcl_entry;
401+
const std::chrono::steady_clock::time_point chrono_until =
402+
chrono_entry + std::chrono::nanoseconds(delta_t.nanoseconds());
403+
404+
return cv_.wait_until(lock, chrono_until, pred);
405+
}
406+
407+
408+
bool
409+
wait_until_ros_time(
410+
std::unique_lock<std::mutex> & lock,
411+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred)
412+
{
413+
// Install jump handler for any amount of time change, for two purposes:
414+
// - if ROS time is active, check if time reached on each new clock sample
415+
// - Trigger via on_clock_change to detect if time source changes, to invalidate sleep
416+
rcl_jump_threshold_t threshold;
417+
threshold.on_clock_change = true;
418+
// 0 is disable, so -1 and 1 are smallest possible time changes
419+
threshold.min_backward.nanoseconds = -1;
420+
threshold.min_forward.nanoseconds = 1;
421+
422+
time_source_changed_ = false;
423+
424+
post_time_jump_callback = [this, &lock] (const rcl_time_jump_t & jump)
425+
{
426+
if (jump.clock_change != RCL_ROS_TIME_NO_CHANGE) {
427+
std::lock_guard<std::mutex> lk(*lock.mutex());
428+
time_source_changed_ = true;
429+
}
430+
cv_.notify_one();
431+
};
432+
433+
// Note this is a trade-off. Adding the callback for every call
434+
// is expensive for high frequency calls. For low frequency waits
435+
// its more overhead to have the callback being called all the time.
436+
// As we expect the use case to be low frequency calls to wait_until
437+
// with relative big pauses between the calls, we install it on demand.
438+
auto clock_handler = clock_->create_jump_callback(
439+
nullptr,
440+
post_time_jump_callback,
441+
threshold);
442+
443+
if (!clock_->ros_time_is_active()) {
444+
auto system_time = std::chrono::system_clock::time_point(
445+
// Cast because system clock resolution is too big for nanoseconds on some systems
446+
std::chrono::duration_cast<std::chrono::system_clock::duration>(
447+
std::chrono::nanoseconds(abs_time.nanoseconds())));
448+
449+
return cv_.wait_until(lock, system_time, [this, &pred] () {
450+
return time_source_changed_ || pred();
451+
});
452+
}
453+
454+
// RCL_ROS_TIME with ros_time_is_active.
455+
// Just wait without "until" because installed
456+
// jump callbacks wake the cv on every new sample.
457+
cv_.wait(lock, [this, &pred, &abs_time] () {
458+
return clock_->now() >= abs_time || time_source_changed_ || pred();
459+
});
460+
461+
return clock_->now() < abs_time;
462+
}
463+
464+
public:
465+
explicit ClockWaiterImpl(const rclcpp::Clock::SharedPtr & clock)
466+
:clock_(clock)
467+
{
468+
}
469+
470+
bool
471+
wait_until(
472+
std::unique_lock<std::mutex> & lock,
473+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred)
474+
{
475+
switch(clock_->get_clock_type()) {
476+
case RCL_CLOCK_UNINITIALIZED:
477+
throw std::runtime_error("Error, wait on uninitialized clock called");
478+
case RCL_ROS_TIME:
479+
return wait_until_ros_time(lock, abs_time, pred);
480+
break;
481+
case RCL_STEADY_TIME:
482+
return wait_until_steady_time(lock, abs_time, pred);
483+
break;
484+
case RCL_SYSTEM_TIME:
485+
return wait_until_system_time(lock, abs_time, pred);
486+
break;
487+
}
488+
489+
return false;
490+
}
491+
492+
void
493+
notify_one()
494+
{
495+
cv_.notify_one();
496+
}
497+
};
498+
499+
ClockWaiter::ClockWaiter(const rclcpp::Clock::SharedPtr & clock)
500+
:impl_(std::make_unique<ClockWaiterImpl>(clock))
501+
{
502+
}
503+
504+
ClockWaiter::~ClockWaiter() = default;
505+
506+
bool
507+
ClockWaiter::wait_until(
508+
std::unique_lock<std::mutex> & lock,
509+
const rclcpp::Time & abs_time, const std::function<bool ()> & pred)
510+
{
511+
return impl_->wait_until(lock, abs_time, pred);
512+
}
513+
514+
void
515+
ClockWaiter::notify_one()
516+
{
517+
impl_->notify_one();
518+
}
519+
520+
class ClockConditionalVariable::Impl
521+
{
522+
std::mutex pred_mutex_;
523+
bool shutdown_ = false;
524+
rclcpp::Context::SharedPtr context_;
525+
rclcpp::OnShutdownCallbackHandle shutdown_cb_handle_;
526+
ClockWaiter::UniquePtr clock_;
527+
528+
public:
529+
Impl(const rclcpp::Clock::SharedPtr & clock, rclcpp::Context::SharedPtr context)
530+
:context_(context),
531+
clock_(std::make_unique<ClockWaiter>(clock))
532+
{
533+
if (!context_ || !context_->is_valid()) {
534+
throw std::runtime_error("context cannot be slept with because it's invalid");
535+
}
536+
// Wake this thread if the context is shutdown
537+
shutdown_cb_handle_ = context_->add_on_shutdown_callback(
538+
[this]() {
539+
{
540+
std::unique_lock lock(pred_mutex_);
541+
shutdown_ = true;
542+
}
543+
clock_->notify_one();
544+
});
545+
}
546+
547+
~Impl()
548+
{
549+
context_->remove_on_shutdown_callback(shutdown_cb_handle_);
550+
}
551+
552+
bool
553+
wait_until(
554+
std::unique_lock<std::mutex> & lock, rclcpp::Time until,
555+
const std::function<bool ()> & pred)
556+
{
557+
if(lock.mutex() != &pred_mutex_) {
558+
throw std::runtime_error(
559+
"ClockConditionalVariable::wait_until: Internal error, given lock does not use"
560+
" mutex returned by this->mutex()");
561+
}
562+
563+
clock_->wait_until(lock, until, [this, &pred] () -> bool {
564+
return shutdown_ || pred();
565+
});
566+
return true;
567+
}
568+
569+
void
570+
notify_one()
571+
{
572+
clock_->notify_one();
573+
}
574+
575+
std::mutex &
576+
mutex()
577+
{
578+
return pred_mutex_;
579+
}
580+
};
581+
582+
ClockConditionalVariable::ClockConditionalVariable(
583+
const rclcpp::Clock::SharedPtr & clock,
584+
rclcpp::Context::SharedPtr context)
585+
:impl_(std::make_unique<Impl>(clock, context))
586+
{
587+
}
588+
589+
ClockConditionalVariable::~ClockConditionalVariable() = default;
590+
591+
void
592+
ClockConditionalVariable::notify_one()
593+
{
594+
impl_->notify_one();
595+
}
596+
597+
bool
598+
ClockConditionalVariable::wait_until(
599+
std::unique_lock<std::mutex> & lock, rclcpp::Time until,
600+
const std::function<bool ()> & pred)
601+
{
602+
return impl_->wait_until(lock, until, pred);
603+
}
604+
605+
std::mutex &
606+
ClockConditionalVariable::mutex()
607+
{
608+
return impl_->mutex();
609+
}
610+
370611
} // namespace rclcpp

rclcpp/test/rclcpp/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ ament_add_test_label(test_clock mimick)
6262
if(TARGET test_clock)
6363
target_link_libraries(test_clock ${PROJECT_NAME} mimick ${rcl_interfaces_TARGETS} ${test_msgs_TARGETS})
6464
endif()
65+
ament_add_gtest(test_clock_conditional test_clock_conditional.cpp)
66+
ament_add_test_label(test_clock_conditional mimick)
67+
if(TARGET test_clock_conditional)
68+
target_link_libraries(test_clock_conditional ${PROJECT_NAME} mimick ${rcl_interfaces_TARGETS} ${test_msgs_TARGETS})
69+
endif()
6570
ament_add_gtest(test_copy_all_parameter_values test_copy_all_parameter_values.cpp)
6671
if(TARGET test_copy_all_parameter_values)
6772
target_link_libraries(test_copy_all_parameter_values ${PROJECT_NAME})

0 commit comments

Comments
 (0)