AIStatefulTask ‐ Asynchronous, Stateful Task Scheduler library.

Threads-like task objects evolving through user-defined states.

C++ Prerequisites

Library initialization

Once the build system has been set up correctly (see Compiling) there are still a few things to be taken care of at the C++ level; lets write a quick 'Hello World'.

In fact, a lot of prerequisites are due to libcwd:

  1. Each source file must start with #include "sys.h". By default this will use the one provided by cwds, or you can override that by providing your own sys.h in the root of the project.
  2. Each source or header file that uses debugging, so that certainly includes main.cpp, needs to #include "debug.h" somewhere. However, main.cpp also needs to #include "evio/EventLoop.h" (which includes threadpool/AIThreadPool.h), or at the very least threadpool/AIThreadPool.h if you don't use an EventLoop, which already include(s) debug.h, so including just the former is sufficient.
  3. At the beginning of main you need to add the line:
    Debug(NAMESPACE_DEBUG::init());
    This calls a function provided by cwds that reads your ~/.libcwdrc file turning on debug output for the specified debug channels.

To enable debug output of AIStatefulTask you should turn on the debug channel statefultask. You could add, below the previous one, the line Debug(if (!dc::statefultask.is_on()) dc::statefultask.on()); but prefered is to simply turn on and off debug channels in your ~/.libcwdrc. Mine looks like this:

silent = on channels_default = off channels_on = warning,notice,system,statefultask,threadpool #channels_on = action,semaphore,semaphorestats gdb = /usr/bin/gdb $GDBCOMMANDFILE xterm = konsole --nofork --workdir "$PWD" --geometry 165x24-0-0 -e %s

AIStatefulTask requires multiple initializations at the beginning of main. The reason for that is to make it easier to customize the initialization; although each have sensible defaults you can pass (different) arguments to fine tune the initialization. See their respective documentation for the details. For now don't worry about this and just use the defaults.

The following objects should be created at the start of main:

  • An AIMemoryPagePool object. Causes a singleton object to be initialized that can only be used after this AIMemoryPagePool object is created. You must create it at the start of main however and not as a global variable, so that upon leaving main the singleton is destructed and allocated memory is cleanly deallocated. This is a general approach of the library, and should be of your code that uses the library: do not use the library from anything that can be executed before or after main.
  • An AIThreadPool object. Even though the thread pool has its own separate git submodule, it is the work horse of AIStatefulTask. Like with AIMemoryPagePool, creating an AIThreadPool object at the start of main causes a singleton object to be initialized that can only be used while this object exists. Do not use AIThreadPool::instance() before or after main.
  • One or more AIThreadPool::PriorityQueue objects, refered to by AIQueueHandle`s, to be used as default handler for your tasks. But even if you don't need a handler, you must created at least one queue.
  • Create zero or more AIEngine objects and use those as default handler for your tasks; and have a main loop calling mainloop(); on each of those engines on a regular basis; for example as part of the 'idle' cycle of the main loop of gtkmm/glibmm.

See the helloworld.cxx file of the ai-statefultask-testsuite project for a working "Hello World" example. As another example have a look at the fibonacci.cxx file in the same project.

If you are new to AIStatefulTask then I urge you to first clone ai-statefultask-testsuite and get that working; as that will require you to have all the bits and pieces for building a project that uses AIStatefulTask installed (note: if you use autotools then start with getting, compiling and installing libcwd. Configure libcwd (for example) with --enable-maintainer-mode --enable-optimize --disable-alloc --disable-location --disable-nonthreading because, respectivily, you got it with git, but you're not debugging libcwd itself, you don't need memory allocation debugging (slow), or printing source_file:line_number locations, and we only need the multithreaded version. This is not necessary when you use cmake and set the environment variable GITACHE_ROOT (see Compiling)).

If you run into any problems configuring, compiling and/or installing libcwd using GNU autotools, please watch this tutorial. If you run into any problems with setting up the environment related to the git submodules (cwm4 and cwds), please watch this tutorial.

The minimal program to print debug output looks something like this:

#include "sys.h"
#include "debug.h"
int main()
{
Debug(NAMESPACE_DEBUG::init());
Dout(dc::notice, "Entering main()");
// See below.
Dout(dc::notice, "Leaving main()");
}

which requires (linking with object code from) submodule cwds (which in turn requires libcwd_r).

A minimal program that has a thread pool, looks something like this:

#include "sys.h"
#include "threadpool/AIThreadPool.h" // Includes debug.h.
int main()
{
Debug(NAMESPACE_DEBUG::init());
Dout(dc::notice, "Entering main()");
AIThreadPool thread_pool; // AIThreadPool(int number_of_threads, int max_number_of_threads)
AIQueueHandle high_priority_queue =
thread_pool.new_queue(8); // new_queue(int capacity, int reserved_threads)
// Optionally add more queues for different priorities:
AIQueueHandle medium_priority_queue = thread_pool.new_queue(8);
AIQueueHandle low_priority_queue = thread_pool.new_queue(8);
// See below.
Dout(dc::notice, "Leaving main()");
}

which requires, additionally, submodules threadpool threadsafe and utils.

A minimal program that has an event loop (for I/O), looks something like this:

#include "sys.h"
#include "evio/EventLoop.h"
#include "utils/debug_ostream_operators.h" // Needed to write error to Dout.
int main()
{
Debug(NAMESPACE_DEBUG::init());
Dout(dc::notice, "Entering main()");
AIThreadPool thread_pool;
AIQueueHandle low_priority_queue = thread_pool.new_queue(8);
try
{
evio::EventLoop event_loop(low_priority_queue);
// See below.
// Terminate application.
event_loop.join();
}
catch (AIAlert::Error const& error)
{
Dout(dc::warning, error);
}
Dout(dc::notice, "Leaving main()");
}

which requires, additionally, submodule evio.

When you actually use a task, so far we only used submodules other than statefultask, it is a good idea to also create the AIMemoryPagePool object, just in case (although this is really only required when you use AIStatefulTaskMutex (indirectly)).

Assume we have a task task::HelloWorld and want to run that, after initialization. Then we can run this task in the thread pool:

#include "sys.h"
#include "helloworld-task/HelloWorld.h"
#include "threadpool/AIThreadPool.h"
#include "utils/AIAlert.h"
#include "utils/debug_ostream_operators.h" // Needed to write error to Dout.
int main()
{
Debug(NAMESPACE_DEBUG::init());
Dout(dc::notice, "Entering main()");
AIMemoryPagePool mpp; // Create before thread_pool.
AIThreadPool thread_pool;
AIQueueHandle low_priority_queue = thread_pool.new_queue(8);
try
{
auto task = statefultask::create<task::HelloWorld>();
task->initialize(42);
task->run(low_priority_queue); // Must have a non-immediate handler because it calls yield().
}
catch (AIAlert::Error const& error)
{
Dout(dc::warning, error);
}
Dout(dc::notice, "Leaving main()");
}
Declaration of class DefaultMemoryPagePool.
Definition: DefaultMemoryPagePool.h:116

which requires, additionally, submodules statefultask (and helloworld-task).

Note that AIMemoryPagePool is created before AIThreadPool. This is important because tasks can continue running until the thread pool is destructed, so mpp must be destructed after thread_pool!

Also note that the task is run by passing a handler. Try what happens when you don't pass anything and just call run().

It is also possible to run a task in an AIEngine. An AIEngine requires a main loop somewhere to be run from, which we don't have in a small Hello World type program, so we have to add one for demonstration purposes. I'm also adding the event loop back, pretending we're doing I/O.

#include "sys.h"
#include "helloworld-task/HelloWorld.h" // task::HelloWorld (and task::create).
#include "statefultask/DefaultMemoryPagePool.h" // AIMemoryPagePool.
#include "statefultask/AIEngine.h" // AIEngine, AIThreadPool, AIQueueHandle.
#include "evio/EventLoop.h" // evio::EventLoop (and AIAlert).
#include "utils/debug_ostream_operators.h" // Needed to write AIAlert::Error to Dout.
int main()
{
Debug(NAMESPACE_DEBUG::init()); // Initialize cwds and libcwd.
Dout(dc::notice, "Entering main()");
AIMemoryPagePool mpp; // Create first. Takes optional initialization.
AIThreadPool thread_pool; // Required. Takes optional initialization).
AIQueueHandle low_priority_queue = // At least one queue is required.
thread_pool.new_queue(8);
try
{
evio::EventLoop event_loop(low_priority_queue); // Needed when doing I/O.
AIEngine engine("main engine", 2.0); // To run tasks from a known point
// in some (libraries) main loop.
bool test_finished = false;
// All tasks are created (somewhere, often as member variable of an object).
auto task = statefultask::create<task::HelloWorld>();
// Often initialized before (re)running them.
task->initialize(42);
// And finally started by calling one of the run() member functions.
task->run(&engine, [&](bool CWDEBUG_ONLY(success)){
test_finished = true;
Dout(dc::notice, "Inside the call-back (" <<
(success ? "success" : "failure") << ").");
});
// Destruction of a task is automatic; once both, the 'task' variable
// is destructed and it finished running.
// Add an artificial main loop here.
while (!test_finished)
{
// Run tasks from a known point in some libraries main loop.
// This 'synchronizes' the task with the code of that particular library.
engine.mainloop();
// Pretend we're doing something else too.
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Application termination (this signals clean termination, as opposed to
// leaving this scope by thrown exception).
// It influences how the destructor of evio::EventLoop behaves.
event_loop.join();
}
catch (AIAlert::Error const& error)
{
// This is only debug output. Normally you'd want to handle errors of course.
Dout(dc::warning, error);
}
Dout(dc::notice, "Leaving main()");
}
Declaration of class AIEngine.
Definition: AIEngine.h:110

Linking

Using cmake, each submodule is an OBJECT library and needs to be added to target_link_libraries of the final executable (or shared library if you'd make that instead). To make that easier, they all automatically add themselves to the cmake variable AICXX_OBJECTS_LIST, so you only have to link with that. Task submodules are not added at the moment, so those have to be added specifically.

For example the above would have the following entry in CMakeLists.txt:

add_executable(helloworld helloworld.cxx)
target_link_libraries(helloworld PRIVATE AICxx::helloworld-task ${AICXX_OBJECTS_LIST})

while the src/Makefile.am when using GNU autotools would contain:

helloworld_SOURCES = helloworld.cxx
helloworld_CXXFLAGS = @LIBCWD_R_FLAGS@
helloworld_LDADD = ../helloworld-task/libhelloworldtask.la ../statefultask/libstatefultask.la ../evio/libevio.la ../threadpool/libthreadpool.la ../threadsafe/libthreadsafe.la ../utils/libutils_r.la ../cwds/libcwds_r.la