An open API service indexing awesome lists of open source software.

https://github.com/endurodave/asynccallback

C++ Asynchronous Multicast Callbacks
https://github.com/endurodave/asynccallback

asynchronous-programming callbacks cpp multi-threading

Last synced: 5 days ago
JSON representation

C++ Asynchronous Multicast Callbacks

Awesome Lists containing this project

README

        

![License MIT](https://img.shields.io/github/license/BehaviorTree/BehaviorTree.CPP?color=blue)
[![conan Ubuntu](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_ubuntu.yml/badge.svg)](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_ubuntu.yml)
[![conan Ubuntu](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_clang.yml/badge.svg)](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_clang.yml)
[![conan Windows](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_windows.yml/badge.svg)](https://github.com/endurodave/AsyncCallback/actions/workflows/cmake_windows.yml)

# Asynchronous Multicast Callbacks with Inter-Thread Messaging

A C++ asynchronous callback framework simplifies passing data between threads.

# Table of Contents

- [Asynchronous Multicast Callbacks with Inter-Thread Messaging](#asynchronous-multicast-callbacks-with-inter-thread-messaging)
- [Table of Contents](#table-of-contents)
- [Preface](#preface)
- [Introduction](#introduction)
- [Callbacks Background](#callbacks-background)
- [Using the Code](#using-the-code)
- [SysData Example](#sysdata-example)
- [SysDataClient Example](#sysdataclient-example)
- [SysDataNoLock Example](#sysdatanolock-example)
- [Timer Example](#timer-example)
- [Callback Signature Limitations](#callback-signature-limitations)
- [Implementation](#implementation)
- [Heap](#heap)
- [Porting](#porting)
- [Code Size](#code-size)
- [Asynchronous Library Comparison](#asynchronous-library-comparison)
- [References](#references)

# Preface

Originally published on CodeProject at Asynchronous Multicast Callbacks with Inter-Thread Messaging with a 4.9 out of 5.0 user rating.

CMake is used to create the build files. CMake is free and open-source software. Windows, Linux and other toolchains are supported. See the CMakeLists.txt file for more information.

# Introduction

Callbacks are a powerful concept used to reduce the coupling between two pieces of code. On a multithreaded system, callbacks have limitations. What I've always wanted was a callback mechanism that crosses threads and handles all the low-level machinery to get my event data from one thread to another safety. I need a small, portable and easy to use framework. No more monster switch statements inside a thread loop that typecast OS message queue void* values based upon an enumeration. Create a callback. Register a callback. And the framework automagically invokes the callback with data arguments on a user specified target thread is the goal. 

The callback solution presented here provides the following features:



  1. Asynchronous callbacks – support asynchronous callbacks to and from any thread


  2. Thread targeting – specify the destination thread for the asynchronous callback


  3. Callbacks – invoke any C or C++ free function with a matching signature


  4. Type safe – user defined, type safe callback function data arguments


  5. Member functions – call instance member functions


  6. Multicast callbacks – store multiple callbacks within a list for sequential invocation


  7. Thread-safe – suitable for use on a multi-threaded system


  8. Compact – small, easy to maintain code base consuming minimal code space


  9. Portable – portable to an embedded or PC-based platform


  10. Any compiler – no advanced C++ language features


  11. Any OS - easy porting to any operating system


  12. Elegant syntax – intuitive and easy to use

The callback paradigm significantly eases multithreaded application development by placing the callback and callback data onto the thread of control that you specify. Exposing an asynchronous callback interface for a single class, module or an entire subsystem is extremely easy. The framework is no more difficult to use than a standard C callback but with more features.

# Callbacks Background

The idea of a function callback is very useful. In callback terms, a publisher defines the callback signature and allows anonymous registration of a callback function pointer. A subscriber creates a function implementation conforming to the publisher's callback signature and registers a callback function pointer with the publisher at runtime. The publisher code knows nothing about the subscriber code – the registration and the callback invocation is anonymous.

Now, on a multithreaded system, you need understand synchronous vs. asynchronous callback invocations. If the callback is synchronous, the callback is executed on the caller's thread of control. If you put a break point inside the callback, the stack frame will show the publisher function call and the publisher callback all synchronously invoked. There are no multithreaded issues with this scenario as everything is running on a single thread.

If the publisher code has its own thread, it may invoke the callback function on its thread of control and not the subscriber's thread. A publisher invoked callback can occur at any time completely independent of the subscriber’s thread of control. This cross-threading can cause problems for the subscriber if the callback code is not thread-safe since you now have another thread calling into subscriber code base at some unknown interval.

One solution for making a callback function thread-safe is to post a message to the subscriber's OS queue during the publisher's callback. The subscriber's thread later dequeues the message and calls an appropriate function. Since the callback implementation only posts a message, the callback, even if done asynchronously, is thread-safe. In this case, the asynchrony of a message queue provides the thread safety in lieu of software locks.

Callbacks are typically free functions, either a class static member or a global function. In C++, instance member functions are handled differently and have significant limitations when it comes to member function pointers. I won't go into all the sorted details, the topic has been covered endlessly elsewhere, but suffice to say you can't have a single pointer point to all function types. This framework supports calling free functions, but offers support to get the call back onto an instance member function.

# Using the Code

I'll first present how to use the code, and then get into the implementation details.

A publisher uses the AsycCallback<> class to expose a callback interface to potential subscribers. An instance is created with one template argument – the user data type for function callback argument. In the example below, an int will become the callback function argument.


AsyncCallback<int> callback;

To subscribe to callback, create a free function (static member or global) as shown. I’ll explain why the <int> argument requires a (const int&, void*) function signature shortly.


void SimpleCallback(const int& value, void* userData)
{
cout << "SimpleCallback " << value << endl;
}

The subscriber registers to receive callbacks using the Register() function. The first argument is a pointer to the callback function. The second argument is a pointer to a thread the callback is to be invoked on.


callback.Register(&SimpleCallback, &workerThread1);

When the publisher needs to invoke the callback for all registered subscribers, use operator() or Invoke(). Neither function executes the callback synchronously; instead it dispatches each callback onto the destination thread of control.


callback(123);
callback.Invoke(123);

Use Unregister() to unsubscribe a callback.


callback.Unregister(&SimpleCallback, &workerThread1);

Alternatively, to unregister all callbacks use Clear().


callback.Clear();

Always check if anyone is subscribed to the callback before invocation using one of these two methods.


if (callback)
callback(123);
if (!callback.Empty())
callback(123);

An AsyncCallback<> is easily used to add asynchrony to both incoming and outgoing API interfaces. The following examples show how.

## SysData Example

SysData is a simple class showing how to expose an outgoing asynchronous interface. The class stores system data and provides asynchronous subscriber notifications when the mode changes. The class interface is shown below.


class SysData
{
public:
/// Clients register with AsyncCallback to get callbacks when system mode changes
AsyncCallback<SystemModeChanged> SystemModeChangedCallback;

/// Get singleton instance of this class
static SysData& GetInstance();

/// Sets the system mode and notify registered clients via SystemModeChangedCallback.
/// @param[in] systemMode - the new system mode.
void SetSystemMode(SystemMode::Type systemMode);

private:
SysData();
~SysData();

/// The current system mode data
SystemMode::Type m_systemMode;

/// Lock to make the class thread-safe
LOCK m_lock;
};

The subscriber interface for receiving callbacks is SystemModeChangedCallback. Calling SetSystemMode() saves the new mode into m_systemMode and notifies all registered subscribers.


void SysData::SetSystemMode(SystemMode::Type systemMode)
{
LockGuard lockGuard(&m_lock);

// Create the callback data
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;

// Update the system mode
m_systemMode = systemMode;

// Callback all registered subscribers
if (SystemModeChangedCallback)
SystemModeChangedCallback(callbackData);
}

## SysDataClient Example

SysDataClient is a callback subscriber and registers for notifications within the constructor. Notice the third argument to Register() is a this pointer. The pointer is passed back as userData on each callback. The framework internally does nothing with userData other that pass it back to the callback invocation. The userData value can be anything the caller wants.


// Constructor
SysDataClient() :
m_numberOfCallbacks(0)
{
// Register for async callbacks
SysData::GetInstance().SystemModeChangedCallback.Register(&SysDataClient::CallbackFunction,
&workerThread1, this);
}

SysDataClient::CallbackFunction() is now called when the system mode changes. Note that the userData argument is typecast back to a SysDataClient instance. Since Register() provided a this pointer, the callback function is able to access any object instance or function during execution.


static void CallbackFunction(const SystemModeChanged& data, void* userData)
{
// The user data pointer originates from the 3rd argument in the Register() function
// Typecast the void* to SysDataClient* to access object instance data/functions.
SysDataClient* instance = static_cast<SysDataClient*>(userData);
instance->m_numberOfCallbacks++;

cout << "CallbackFunction " << data.CurrentSystemMode << endl;
}

When SetSystemMode() is called, anyone interested in the mode changes are notified asynchronously on their desired execution thread.


// Set new SystemMode values. Each call will invoke callbacks to all
// registered client subscribers.
SysData::GetInstance().SetSystemMode(SystemMode::STARTING);
SysData::GetInstance().SetSystemMode(SystemMode::NORMAL);

## SysDataNoLock Example

SysDataNoLocks is an alternate implementation that uses a private AsyncCallback<> for setting the system mode asynchronously and without locks.


class SysDataNoLock
{
public:
    /// Clients register with AsyncCallback to get callbacks when system mode changes
    AsyncCallback<SystemModeChanged> SystemModeChangedCallback;

    /// Get singleton instance of this class
    static SysDataNoLock& GetInstance();

    /// Sets the system mode and notify registered clients via SystemModeChangedCallback.
    /// @param[in] systemMode - the new system mode. 
    void SetSystemMode(SystemMode::Type systemMode);    

private:
    SysDataNoLock();
    ~SysDataNoLock();

    /// Private callback to get the SetSystemMode call onto a common thread
    AsyncCallback<SystemMode::Type> SetSystemModeCallback;

    /// Sets the system mode and notify registered clients via SystemModeChangedCallback.
    /// @param[in] systemMode - the new system mode. 
    /// @param[in] userData - a 'this' pointer to SysDataNoLock.  
    static void SetSystemModePrivate(const SystemMode::Type& systemMode, void* userData);    

    /// The current system mode data
    SystemMode::Type m_systemMode;
};

The constructor registers SetSystemModePrivate() with the private SetSystemModeCallback.


SysDataNoLock::SysDataNoLock() :
m_systemMode(SystemMode::STARTING)
{
SetSystemModeCallback.Register(&SysDataNoLock::SetSystemModePrivate, &workerThread2, this);
workerThread2.CreateThread();
}

The SetSystemMode() function below is an example of an asynchronous incoming interface. To the caller, it looks like a normal function, but under the hood, a private member call is invoked asynchronously. In this case, invoking SetSystemModeCallback causes SetSystemModePrivate() to be called on workerThread2.


void SysDataNoLock::SetSystemMode(SystemMode::Type systemMode)
{
    // Invoke the private callback. SetSystemModePrivate() will be called on workerThread2.
    SetSystemModeCallback(systemMode);
}

Since this private function is always invoked asynchronously on workerThread2 it doesn't require locks.


void SysDataNoLock::SetSystemModePrivate(const SystemMode::Type& systemMode, void* userData)
{
    SysDataNoLock* instance = static_cast<SysDataNoLock*>(userData);

    // Create the callback data
    SystemModeChanged callbackData;
    callbackData.PreviousSystemMode = instance->m_systemMode;
    callbackData.CurrentSystemMode = systemMode;

    // Update the system mode
    instance->m_systemMode = systemMode;

    // Callback all registered subscribers
    if (instance->SystemModeChangedCallback)
        instance->SystemModeChangedCallback(callbackData);
}

## Timer Example

Once a callback framework is in place, creating a timer callback service is trivial. Many systems need a way to generate a callback based on a timeout. Maybe it's a periodic timeout for some low speed polling, or maybe an error timeout in case something doesn't occur within the expected time frame. Either way, the callback must occur on a specified thread of control. An AsyncCallback<> used inside a Timer class solves this nicely.


class Timer
{
public:
AsyncCallback<TimerData> Expired;

void Start(UINT32 timeout);
void Stop();
//...
};

Users create an instance of the timer and register for the expiration. In this case, MyCallback() is called in 1000ms.


m_timer.Expired.Register(&MyClass::MyCallback, &myThread, this);
m_timer.Start(1000);

A Timer implementation isn't offered in the examples. However, the article "C++ State Machine with Threads" contains a Timer class that shows a complete multithreaded example of AsyncCallback<> integrated with a C++ state machine.

# Callback Signature Limitations

This design has the following limitations imposed on all callback functions:


  1. Each callback handles a single user defined argument type (TData).

  2. The two callback function arguments are always: const TData& and void*.

  3. Each callback has a void return type.

For instance, if an AsyncCallback<> is declared as:


AsyncCallback<MyData> myCallback;

The callback function signature is:


void MyCallback(const MyData& data, void* userData);

The design can be extended to support more than one argument if necessary. However, the design somewhat mimics what embedded programmers do all the time, which is something like:


  1. Dynamically create an instance to a struct or class and populate data.

  2. Post a pointer to the data through an OS message as a void*.

  3. Get the data from the OS message queue and typecast the void* back to the original type.

  4. Delete the dynamically created data.

In this design, the entire infrastructure happens automatically without any additional effort on the programmer's part. If multiple data parameters are required, they must be packaged into a single class/struct and used as the callback data argument.

# Implementation

The number of lines of code for the callback framework is surprisingly low. Strip out the comments, and maybe a couple hundred lines of code that are (hopefully) easy to understand and maintain.

AsyncCallback<> and AsyncCallbackBase form the basis for publishing a callback interface. The classes are thread-safe. The base version is non-templatized to reduce code space. AsyncCallbackBase provides the invocation list and thread safety mechanisms.

AsyncCallback::Invoke() iterates over the list and dispatches callback messages to each target thread. The data is dynamically created to travel through an OS message queue.


void Invoke(const TData& data)
{
LockGuard lockGuard(GetLock());

// For each registered callback
InvocationNode* node = GetInvocationHead();
while (node != NULL)
{
// Create a new instance of callback and copy
const Callback* callback = new Callback(*node->CallbackElement);

// Create a new instance of the callback data and copy
const TData* callbackData = new TData(data);

// Create a new message instance
CallbackMsg* msg = new CallbackMsg(this, callback, callbackData);

// Dispatch message onto the callback destination thread. TargetInvoke()
// will be called by the target thread.
callback->GetCallbackThread()->DispatchCallback(msg);

// Get the next registered callback subscriber
node = node->Next;
}
}

AsyncCallback::TargetInvoke() is called by target thread to actually execute the callback. Dynamic data is deleted after the callback is invoked.


virtual void TargetInvoke(CallbackMsg** msg) const
{
const Callback* callback = (*msg)->GetCallback();

// Typecast the void* back to a TData type
const TData* callbackData = static_cast<const TData*>((*msg)->GetCallbackData());

// Typecast a generic callback function pointer to the CallbackFunc type
CallbackFunc func = reinterpret_cast<CallbackFunc>(callback->GetCallbackFunction());

// Execute the registered callback function
(*func)(*callbackData, callback->GetUserData());

// Delete dynamically data sent through the message queue
delete callbackData;
delete callback;
delete *msg;
*msg = NULL;
}

Asynchronous callbacks impose certain limitations because everything the callback destination thread needs must be created on the heap, packaged into a class, and placed into an OS message queue.

The insertion into an OS queue is platform specific. The CallbackThread class provides the interface to be implemented on each target platform. See the Porting section below for a more complete discussion.


class CallbackThread
{
public:
virtual void DispatchCallback(CallbackMsg* msg) = 0;
};

Once the message is placed into the message queue, platform specific code unpacks the message and calls the AsyncCallbackBase::TargetInvoke() function and destroys dynamically allocated data.


unsigned long WorkerThread::Process(void* parameter)
{
MSG msg;
BOOL bRet;
while ((bRet = GetMessage(&msg, NULL, WM_USER_BEGIN, WM_USER_END)) != 0)
{
switch (msg.message)
{
case WM_DISPATCH_CALLBACK:
{
ASSERT_TRUE(msg.wParam != NULL);

// Get the ThreadMsg from the wParam value
ThreadMsg* threadMsg = reinterpret_cast<ThreadMsg*>(msg.wParam);

// Convert the ThreadMsg void* data back to a CallbackMsg*
CallbackMsg* callbackMsg = static_cast<CallbackMsg*>(threadMsg->GetData());

// Invoke the callback callback on the target thread
callbackMsg->GetAsyncCallback()->TargetInvoke(&callbackMsg);

// Delete dynamic data passed through message queue
delete threadMsg;
break;
}

case WM_EXIT_THREAD:
return 0;

default:
ASSERT();
}
}
return 0;
}

Notice the thread loop is unlike most systems that have a huge switch statement handling various incoming data messages, type casting void* data, then calling a specific function. The framework supports all callbacks with a single WM_DISPATCH_CALLBACK message. Once setup, the same small thread loop handles every callback. New publisher and subscribers come and go as the system is designed, but the code in-between doesn't change.

This is a huge benefit as on many systems getting data between threads takes a lot of manual steps. You constantly have to mess with each thread loop, create during sending, destroy data when receiving, and call various OS services and typecasts. Here you do none of that. All the stuff in-between is neatly handled for users.

# Heap

The heap is used to create dynamic data. It stems from using an invocation list and needing to send data objects through the message queue. Remember, your callback data is copied and destroyed during a callback. Most times, the callback data is POD (Plain Old Data Structure). If you have something fancier that can't be bitwise copied, be sure to implement a copy constructor for the callback data.

On some systems, it is undesirable to use the heap. For those situations, I use a fixed block memory allocator. The xallocator implementation solves the dynamic storage issues and is much faster than the global heap. To use, just include xallocator.h and add the macro XALLOCATOR to the class declaration. An entire class hierarchy can use the fixed block allocator by placing XALLOCTOR in the base class.


#include "xallocator.h"

class Callback
{
XALLOCATOR
// ...
};

With xallocator in place, calling operator new or delete allows the fixed block allocator to take over the storage duties. How objects are created and destroyed is exactly the same, only the source of the memory is different. For more information on xallocator, and to get the source code, see the article "Replace malloc/free with a Fast Fixed Block Memory Allocator". The only files needed are Allocator.h/cpp and xallocator.h/cpp.

To use xallocator in the callback framework, place XALLOCATOR macros in the following class definitions:


  • Callback

  • CallbackMsg

  • InvocationNode

For the platform specific files, you also include XALLOCATOR. In this example, these are:


  • ThreadMsg

  • SystemModeChanged

# Porting

The code is an easy port to any platform. There are only two OS services required: threads and a software lock. The code is separated into five directories. 



  1. AsyncCallback - core framework implementation files


  2. Port – port-specific files


  3. Examples – sample code showing usage

The library has a single abstract class CallbackThread with a single pure virtual function:


virtual void DispatchCallback(CallbackMsg* msg) = 0;

On most projects, I wrap the underlying raw OS calls into a thread class to encapsulate and enforce the correct behavior. Here, I provide ThreadWin as a wrapper over the CreateThread() Windows API.

Once you have a thread class, just inherit the CallbackThread interface and implement the DispatchCallback() function. On Windows, a simple post to a message queue is all that is required:


void ThreadWin::DispatchCallback(CallbackMsg* msg)
{
// Create a new ThreadMsg
ThreadMsg* threadMsg = new ThreadMsg(WM_DISPATCH_CALLBACK, msg);

// Post the message to the this thread's message queue
PostThreadMessage(WM_DISPATCH_CALLBACK, threadMsg);
}

The Windows thread loop gets the message and calls the TargetInvoke() function for the incoming instance. The data sent through the queue is deleted once complete.


switch (msg.message)
{
case WM_DISPATCH_CALLBACK:
{
ASSERT_TRUE(msg.wParam != NULL);

// Get the ThreadMsg from the wParam value
ThreadMsg* threadMsg = reinterpret_cast<ThreadMsg*>(msg.wParam);

// Convert the ThreadMsg void* data back to a CallbackMsg*
CallbackMsg* callbackMsg = static_cast<CallbackMsg*>(threadMsg->GetData());

// Invoke the callback callback on the target thread
callbackMsg->GetAsyncCallback()->TargetInvoke(&callbackMsg);

// Delete dynamic data passed through message queue
delete threadMsg;
break;
}

case WM_EXIT_THREAD:
return 0;

default:
ASSERT();
}

Software locks are handled by the LockGuard class. This class can be updated with locks of your choice, or you can use a different mechanism. Locks are only used in a few places.

# Code Size

To gauge the cost of using this technique, the code was built for an ARM CPU using Keil. If deployed on a project, many AsyncCallback<> instances will be created so it needs to be space efficient.

The incremental code size of for each additional AsyncCallback<>, one subscriber, one registration call, and one callback invocation is around 120 bytes using full optimization. You’d certainly use at least this much code moving data from one thread to another manually.

# Asynchronous Library Comparison

Asynchronous function invocation allows for easy movement of data between threads. The table below summarizes the various asynchronous function invocation implementations available in C and C++.

| Repository | Language | Key Features | Notes |
|-------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DelegateMQ | C++17 | * Function-like template syntax
* Any delegate target function type (member, static, free, lambda)
* N target function arguments
* N delegate subscribers
* Variadic templates
* Template metaprogramming | * Modern C++
* Invoke synchronously, asynchronously or remotely
* Extensive unit tests
|
| AsyncCallback | C++ | * Traditional template syntax
* Delegate target function type (static, free)
* 1 target function argument
* N delegate subscribers | * Low lines of source code
* Most compact C++ implementation
* Any C++ compiler |
| C_AsyncCallback | C | * Macros provide type-safety
* Delegate target function type (static, free)
* 1 target function argument
* Fixed delegate subscribers (set at compile time)
* Optional fixed block allocator | * Low lines of source code
* Very compact implementation
* Any C compiler

# References