Skip to content

SO 5.8 ByExample Many Timers

Yauheni Akhotnikau edited this page Jul 7, 2023 · 1 revision

Note. It is better to read SO-5.8 Basics before this text.

Introduction

This example shows how user can set the appropriate timer thread for its application.

Some Words About Timer Thread

Most SObjectizer-based applications need timers in the form of delayed or periodic messages (see SO-5.8 By Example Periodic Hello for more details). Timers are handled by timer thread. Timer thread is automatically started as part of SObjectizer Environment startup routine and automatically shut down as part of SObjectizer Environment shutdown procedure. During its work timer thread handles message timeouts and delivers messages to appropriate mboxes when timeouts elapsed. When so_5::send_delayed and so_5::send_periodic invoked then a timer is registered in timer thread and timer thread starts control this timer. When timer is released (for example by so_5::timer_id_t::release() method) it is removed from timer thread.

Since v.5.5.0 the Timer Thread Template library is used for timer thread implementation. The timertt library provides several timer mechanisms. Every of them has its own strengths and weaknesses. So if an application uses timers heavily then it is a task of the user to select most appropriate timer thread.

There are three timer thread types in SObjectizer since v.5.5.0:

  • timer_wheel. This thread uses the most advanced and scalable timer mechanisms. But at the price of consuming of resources even if there is no timers at all. This timer thread the only choice for the case of very big amount of timers (tens of millions or more). But for the simple cases where there are only few thousands timers usage of this timer thread type can be overkill.
  • timer_heap. This thread uses a binary heap data structure which allows to handle timers efficiently. This thread sleeps between timer events and does not consume resources if there is no timers at all (unlike timer_wheel thread). But addition and deletion of timers cost more than in the case of timer_wheel thread. Because of that timer_heap thread handles timers about 4-5 timer slower than timer_wheel thread. But if application doesn't use millions of timers this difference is not critical. Because of that timer_heap thread is the default timer thread for SObjectizer Environment.
  • timer_list. This thread uses a simple ordered list of timers. It makes timer_list thread very efficient for rare case: when application uses only timers with the same pauses. In other cases this timer thread will have problems with timer addition: it could take too much time to find an appropriate place for insertion of the new timer into timer's list. But this thread doesn't consume resources if there is a few timers and has very cheap timer deletion. So this thread can be very efficient in some cases.

Timer thread is created at the start of SObjectizer Environment. Timer thread is created by timer_thread_factory function. And user could specify a factory function for the timer thread he/she needs. SObjectizer provides several timer_thread_factory functions and the user can choose the one of them.

What Example Does

The sample creates two active agents. One of them is sender. It sends a specified count of delayed signals with specified pause parameter to the second agent's mbox. The second agent is a receiver. It receives the signals sent by the first agent. When the receiver has received all signals it finishes sample work.

Count of signals to be sent, pause for every signal and the type of timer thread can be specified via command line parameters.

This example shows the efficiency of timer scheduling mechanisms in SObjectizer. But this efficiency is a composition of different factors: there is efficiency of timer thread itself and the efficiency of signal delivery in SObjectizer (especially when receiver is an active agent). There is also efficiency of std::mutex/std::condition_variable implementation (they are used inside timertt library). Because of that results can vary significantly: for example on Windows platform results for Visual C++ compiler for timer_list/timer_heap can be much better than for MinGW GCC compiler (where the adoption of pthreads on Win32 API is used).

Sample Code

Note. It is not full sample code. Some fragments related to command-line arguments are skipped to simplify code example understanding. The full code of this sample can be found inside full SObjectizer source code bundle.

#include <iostream>
#include <cstring>
#include <cstdlib>

#include <so_5/all.hpp>

#if defined(__clang__) && (__clang_major__ >= 16)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#endif

// Configuration for the sample.
struct cfg_t
{
	// Count of messages to be sent.
	unsigned long long m_messages = 5000000;

	// Initial delay for every message.
	std::chrono::milliseconds m_delay = std::chrono::milliseconds{ 100 };

	// Type of timer.
	enum class timer_type_t {
		wheel,
		list,
		heap
	} m_timer_type = { timer_type_t::wheel };
};

/*
 * -----------------------------------------------------------------
 * The source code of str_to_value, parse_args and show_cfg skipped.
 * -----------------------------------------------------------------
 */

// Timer message.
struct msg_timer final : public so_5::signal_t {};

// Agent-receiver.
class a_receiver_t final : public so_5::agent_t
{
public :
	a_receiver_t(
		context_t ctx,
		unsigned long long messages )
		:	so_5::agent_t( ctx )
		,	m_messages_to_receive( messages )
	{}

	void so_define_agent() override
	{
		so_subscribe_self().event( &a_receiver_t::evt_timer );
	}

	void evt_timer(mhood_t< msg_timer >)
	{
		++m_messages_received;
		if( m_messages_received == m_messages_to_receive )
			so_deregister_agent_coop_normally();
	}

private :
	const unsigned long long m_messages_to_receive;
	unsigned long long m_messages_received = { 0 };
};

// Agent-sender.
class a_sender_t final : public so_5::agent_t
{
public :
	a_sender_t(
		context_t ctx,
		so_5::mbox_t dest_mbox,
		unsigned long long messages_to_send,
		std::chrono::milliseconds delay )
		:	so_5::agent_t( ctx )
		,	m_dest_mbox( std::move( dest_mbox ) )
		,	m_messages_to_send( messages_to_send )
		,	m_delay( delay )
	{}

	void so_evt_start() override
	{
		for( unsigned long long i = 0; i != m_messages_to_send; ++i )
			so_5::send_delayed< msg_timer >( m_dest_mbox, m_delay );
	}

private :
	const so_5::mbox_t m_dest_mbox;

	const unsigned long long m_messages_to_send;

	const std::chrono::milliseconds m_delay;
};

void run_sobjectizer( const cfg_t & cfg )
{
	so_5::launch(
		// Initialization actions.
		[&cfg]( so_5::environment_t & env )
		{
			// Active object dispatcher is necessary.
			env.introduce_coop(
				so_5::disp::active_obj::make_dispatcher( env ).binder(),
				[&cfg]( so_5::coop_t & coop ) {
					auto a_receiver = coop.make_agent< a_receiver_t >( cfg.m_messages );

					coop.make_agent< a_sender_t >(
							a_receiver->so_direct_mbox(), cfg.m_messages, cfg.m_delay );
				});
		},
		// Parameter tuning actions.
		[&cfg]( so_5::environment_params_t & params )
		{
			// Appropriate timer thread must be used.
			so_5::timer_thread_factory_t timer = so_5::timer_wheel_factory();
			if( cfg.m_timer_type == cfg_t::timer_type_t::list )
				timer = so_5::timer_list_factory();
			else if( cfg.m_timer_type == cfg_t::timer_type_t::heap )
				timer = so_5::timer_heap_factory();

			params.timer_thread( timer );
		} );
}

int main( int argc, char ** argv )
{
	try
	{
		const auto cfg = parse_args( argc, argv );
		show_cfg( cfg );

		run_sobjectizer( cfg );

		return 0;
	}
	catch( const std::exception & x )
	{
		std::cerr << "Exception caught: " << x.what() << std::endl;
	}

	return 2;
}

Sample in Details

Note. The following is a description of the most important aspect of the example: choice of timer_thread_factory and specifying the factory to SObjectizer Environment's parameters. If the reader doesn't understand other fragments (like delayed message sending or binding agent to active_obj dispatcher) it is recommended to read other introduction materials like SO-5.8 Basics and SO-5.8 By Example Periodic Hello.

Choosing Timer Thread Factory

SObjectizer v.5.5 defines type so_5::timer_thread_factory_t for storing factory objects. Those factories creates appropriate timer thread with default or explicitly specified parameters.

There are some functions which returns the corresponding factories:

  • so_5::timer_wheel_factory() -- creates timer_wheel thread factory with default parameters (wheel size and time step duration);
  • so_5::timer_wheel_factory(wheel_size, granularity) -- creates timer_wheel thread factory for the specified wheel size and timer step duration;
  • so_5::timer_heap_factory() -- creates timer_heap thread factory with default parameters (initial heap-array size);
  • so_5::timer_heap_factory(initial_size) -- creates timer_heap thread factory the specified initial heap-array size;
  • so_5::timer_list_factory() -- creates timer_list thread factory. At the moment this timer thread doesn't use any tuning parameters.

This sample uses three of them -- one for each timer thread type, the default parameters are used for timer_wheel and timer_heap threads:

// Appropriate timer thread must be used.
so_5::timer_thread_factory_t timer = so_5::timer_wheel_factory();
if( cfg.m_timer_type == cfg_t::timer_type_t::list )
    timer = so_5::timer_list_factory();
else if( cfg.m_timer_type == cfg_t::timer_type_t::heap )
    timer = so_5::timer_heap_factory();

The timer thread factory is specified to SObjectizer Environment by so_5::environment_params_t::timer_thread(factory) method:

params.timer_thread( timer );
Clone this wiki locally