1717import rclpy .callback_groups
1818import rclpy .executors
1919import rclpy .node
20+ import rclpy .timer
2021
2122from synchros2 .futures import FutureLike
2223from synchros2 .utilities import bind_to_thread , fqn
@@ -592,6 +593,7 @@ def __init__(
592593 max_thread_idle_time : typing .Optional [float ] = None ,
593594 max_threads_per_callback_group : typing .Optional [int ] = None ,
594595 * ,
596+ num_threads_for_timers : typing .Optional [int ] = None ,
595597 context : typing .Optional [rclpy .context .Context ] = None ,
596598 logger : typing .Optional [logging .Logger ] = None ,
597599 ) -> None :
@@ -607,24 +609,41 @@ def __init__(
607609 max_threads_per_callback_group: optional maximum number of concurrent callbacks the
608610 default thread pool should service for a given callback group. Useful to avoid
609611 reentrant callback groups from starving the default thread pool.
612+ num_threads_for_timers: optional number of threads to dedicate to timer callbacks.
613+ Defaults to 10% of all available threads, which may be 0 if there are less than
614+ 10 threads, in which case timer callbacks will be serviced by the default thread pool.
610615 context: An optional instance of the ros context.
611616 logger: An optional logger instance.
612617 """
613618 super ().__init__ (context = context )
614619 if logger is None :
615620 logger = rclpy .logging .get_logger (fqn (self .__class__ ))
621+ if max_threads is None :
622+ max_threads = 32 * (os .cpu_count () or 1 )
623+ if num_threads_for_timers is None :
624+ num_threads_for_timers = max_threads // 10
625+ if num_threads_for_timers == 0 :
626+ logger .warning ("Not enough threads available, timers will be serviced by the default thread pool" )
627+ max_threads -= num_threads_for_timers
616628 self ._logger = logger
617629 self ._is_shutdown = False
618630 self ._spin_lock = threading .Lock ()
619631 self ._shutdown_lock = threading .RLock ()
620- self ._thread_pools = [
621- AutoScalingThreadPool (
622- max_workers = max_threads ,
623- max_idle_time = max_thread_idle_time ,
632+ self ._default_thread_pool = AutoScalingThreadPool (
633+ max_workers = max_threads ,
634+ max_idle_time = max_thread_idle_time ,
635+ submission_quota = max_threads_per_callback_group ,
636+ logger = self ._logger ,
637+ )
638+ self ._timers_thread_pool : typing .Optional [AutoScalingThreadPool ] = None
639+ if num_threads_for_timers != 0 :
640+ self ._timers_thread_pool = AutoScalingThreadPool (
641+ min_workers = num_threads_for_timers ,
642+ max_workers = num_threads_for_timers ,
624643 submission_quota = max_threads_per_callback_group ,
625644 logger = self ._logger ,
626- ),
627- ]
645+ )
646+ self . _static_thread_pools : typing . List [ AutoScalingThreadPool ] = [ ]
628647 self ._callback_group_affinity : weakref .WeakKeyDictionary [
629648 rclpy .callback_groups .CallbackGroup ,
630649 AutoScalingThreadPool ,
@@ -637,12 +656,21 @@ def __init__(
637656 @property
638657 def default_thread_pool (self ) -> AutoScalingThreadPool :
639658 """Default autoscaling thread pool."""
640- return self ._thread_pools [0 ]
659+ return self ._default_thread_pool
660+
661+ @property
662+ def timers_thread_pool (self ) -> typing .Optional [AutoScalingThreadPool ]:
663+ """Autoscaling thread pool for timer callbacks."""
664+ return self ._timers_thread_pool
641665
642666 @property
643667 def thread_pools (self ) -> typing .List [AutoScalingThreadPool ]:
644668 """Autoscaling thread pools in use."""
645- return list (self ._thread_pools )
669+ thread_pools = [self ._default_thread_pool ]
670+ if self ._timers_thread_pool is not None :
671+ thread_pools .append (self ._timers_thread_pool )
672+ thread_pools .extend (self ._static_thread_pools )
673+ return thread_pools
646674
647675 def add_static_thread_pool (self , num_threads : typing .Optional [int ] = None ) -> AutoScalingThreadPool :
648676 """Add a thread pool that keeps a steady number of workers."""
@@ -653,8 +681,8 @@ def add_static_thread_pool(self, num_threads: typing.Optional[int] = None) -> Au
653681 max_workers = num_threads ,
654682 logger = self ._logger ,
655683 )
656- self ._thread_pools .append (thread_pool )
657- self ._logger .debug (f"Added static thread pool #{ len (self ._thread_pools ) - 1 } " )
684+ self ._static_thread_pools .append (thread_pool )
685+ self ._logger .debug (f"Added static thread pool #{ len (self ._static_thread_pools ) - 1 } " )
658686 return thread_pool
659687
660688 def bind (self , callback_group : rclpy .callback_groups .CallbackGroup , thread_pool : AutoScalingThreadPool ) -> None :
@@ -663,9 +691,13 @@ def bind(self, callback_group: rclpy.callback_groups.CallbackGroup, thread_pool:
663691 Thread pool must be known to the executor. That is, instantiated through add_*_thread_pool() methods.
664692 """
665693 with self ._shutdown_lock :
666- if thread_pool not in self ._thread_pools :
694+ if thread_pool not in self ._static_thread_pools :
695+ if thread_pool is self ._default_thread_pool :
696+ raise ValueError ("cannot rebind to default thread pool" )
697+ if thread_pool is self ._timers_thread_pool :
698+ raise ValueError ("cannot bind to timers thread pool" )
667699 raise ValueError ("thread pool unknown to executor" )
668- thread_pool_index = self ._thread_pools .index (thread_pool )
700+ thread_pool_index = self ._static_thread_pools .index (thread_pool )
669701 callback_group_name = f"{ fqn (type (callback_group ))} @{ id (callback_group )} "
670702 self ._logger .debug (f"Binding { callback_group_name } to thread pool #{ thread_pool_index } ..." )
671703 self ._callback_group_affinity [callback_group ] = thread_pool
@@ -698,14 +730,16 @@ def _do_spin_once(self, *args: typing.Any, **kwargs: typing.Any) -> None:
698730 # dispatch and be missed. Fortunately, this will only delay dispatch until the
699731 # next spin cycle.
700732 if task not in self ._work_in_progress or (self ._work_in_progress [task ].done () and not task .done ()):
701- if task .callback_group is not None :
702- if task .callback_group not in self ._callback_group_affinity :
703- self ._callback_group_affinity [task .callback_group ] = self ._thread_pools [0 ]
733+ if task .callback_group is not None and task .callback_group in self ._callback_group_affinity :
704734 thread_pool = self ._callback_group_affinity [task .callback_group ]
735+ thread_pool_index = self ._static_thread_pools .index (thread_pool )
736+ self ._logger .debug (f"Task '{ task } ' submitted to static thread pool #{ thread_pool_index } " )
737+ elif self ._timers_thread_pool is not None and isinstance (task .entity , rclpy .timer .Timer ):
738+ thread_pool = self ._timers_thread_pool
739+ self ._logger .debug (f"Task '{ task } ' submitted to timers thread pool" )
705740 else :
706- thread_pool = self ._thread_pools [0 ]
707- thread_pool_index = self ._thread_pools .index (thread_pool )
708- self ._logger .debug (f"Task '{ task } ' submitted to thread pool #{ thread_pool_index } " )
741+ thread_pool = self ._default_thread_pool
742+ self ._logger .debug (f"Task '{ task } ' submitted to default thread pool" )
709743 self ._work_in_progress [task ] = thread_pool .submit (task )
710744 for task in list (self ._work_in_progress ):
711745 if not task .done ():
@@ -781,10 +815,11 @@ def shutdown(self, timeout_sec: typing.Optional[float] = None) -> bool:
781815 # must be waited on. Work tracking in rclpy.executors.Executor
782816 # base implementation is subject to races, so block thread pool
783817 # submissions and wait for all futures to finish. Then shutdown.
784- done = all (thread_pool .wait (timeout_sec ) for thread_pool in self ._thread_pools )
818+
819+ done = all (thread_pool .wait (timeout_sec ) for thread_pool in self .thread_pools )
785820 if done :
786821 assert super ().shutdown (timeout_sec = 0 )
787- for thread_pool in self ._thread_pools :
822+ for thread_pool in self .thread_pools :
788823 thread_pool .shutdown ()
789824 self ._is_shutdown = True
790825 if done :
0 commit comments