Back to Index

SignalSlot

Base class for all Task Framework components

Contents

The SignalSlot class implements a thread-safe event notification system that enables loosely coupled communication between components. It provides a mechanism for objects to register interest in events (signals) and receive notifications when those events occur, without requiring direct dependencies between the signal emitter and receiver.

This implementation supports both parameterless signals and signals with arbitrary arguments, providing a powerful foundation for event-driven programming patterns in concurrent environments.

Features

Thread Safety

The SignalSlot implementation is fully thread-safe, with the following guarantees:

Class Interface

class SignalSlot {
public:
    // Constructor & destructor
    SignalSlot(std::ostream &output = std::cerr);
    virtual ~SignalSlot();
    
    // Output stream control
    void setOutputStream(std::ostream &stream);
    std::ostream &stream() const;
    
    // Signal creation
    bool createSignal(const std::string &name);
    
    // Signal checking
    bool hasSignal(const std::string &name) const;
    
    // Unified connection method (auto-detects argument requirements)
    template 
    ConnectionHandle connect(const std::string &name, Func &&slot);
    
    // Connect member function (auto-detects argument requirements)
    template 
    ConnectionHandle connect(const std::string &name, T *instance, Method method);
    
    // Signal emission
    void emit(const std::string &name, SyncPolicy policy = SyncPolicy::Direct);
    void emit(const std::string &name, const ArgumentPack &args,
              SyncPolicy policy = SyncPolicy::Direct);
    void emitString(const std::string &name, const std::string &value,
                    SyncPolicy policy = SyncPolicy::Direct);
    
    // Disconnect all signals
    void disconnectAllSignals();
    
private:
    // Internal signal access methods
    std::shared_ptr getSignal(const std::string &name) const;
    
    // Thread-safe storage
    mutable std::mutex m_signalsMutex;
    std::map> m_signals;
    
    mutable std::mutex m_streamMutex;
    std::reference_wrapper m_output_stream;
};

Signal Types

The updated implementation uses a unified Signal class that can handle both parameterless callbacks and callbacks that take arguments:

Handler Type Description Use Case
void() Simple notification without arguments Status changes, triggers, completion events
void(const ArgumentPack&) Handler with argument data Data updates, errors with details, results

Synchronization Policies

The SignalSlot system supports different synchronization policies for signal emission:

Policy Description Use Case
SyncPolicy::Direct Execute handlers directly in the emitting thread Default behavior, synchronous execution
SyncPolicy::Blocking Block until all handlers have completed When confirmed completion is required
Note: The current implementation executes both policies in the same way (in the emitting thread), but the API is designed to support additional policies in the future, such as asynchronous execution.

Usage Example

Creating and Using Signals
// Class using the SignalSlot system
class FileDownloader : public SignalSlot {
public:
    FileDownloader() {
        // Create signals for various events
        createSignal("downloadStarted");
        createSignal("downloadCompleted");
        createSignal("progress");
        createSignal("error");
    }
    
    void startDownload(const std::string& url) {
        // Signal that download is starting
        emit("downloadStarted");
        
        try {
            // Simulate download process
            for (int i = 0; i <= 10; ++i) {
                // Report progress
                ArgumentPack progressArgs;
                progressArgs.add(i / 10.0f);
                progressArgs.add(url);
                emit("progress", progressArgs);
                
                std::this_thread::sleep_for(std::chrono::milliseconds(100));
            }
            
            // Signal completion
            emit("downloadCompleted");
        }
        catch (const std::exception& e) {
            // Signal error
            emitString("error", std::string("Download failed: ") + e.what());
        }
    }
};
Connecting to Signals
// Using the SignalSlot system
void downloadExample() {
    // Create the downloader
    auto downloader = std::make_shared();
    
    // Connect to signals with lambda functions - no arguments
    downloader->connect("downloadStarted", []() {
        std::cout << "Download has started!" << std::endl;
    });
    
    downloader->connect("downloadCompleted", []() {
        std::cout << "Download completed successfully!" << std::endl;
    });
    
    // Connect to signals with lambda functions - with arguments
    downloader->connect("progress", [](const ArgumentPack& args) {
        float progress = args.get(0);
        std::string url = args.get(1);
        std::cout << "Downloading " << url << ": " 
                 << (progress * 100) << "%" << std::endl;
    });
    
    downloader->connect("error", [](const ArgumentPack& args) {
        std::string errorMsg = args.get(0);
        std::cerr << "ERROR: " << errorMsg << std::endl;
    });
    
    // Start the download
    downloader->startDownload("https://example.com/file.zip");
}
Thread-Safe Usage
// Multi-threaded usage example
void threadedExample() {
    auto signalSource = std::make_shared();
    signalSource->createSignal("event");
    
    // Connect handlers in the main thread
    signalSource->connect("event", [](const ArgumentPack& args) {
        int value = args.get(0);
        std::cout << "Event received with value: " << value << std::endl;
    });
    
    // Create multiple worker threads that emit signals
    std::vector threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([signalSource, i]() {
            for (int j = 0; j < 10; ++j) {
                // Create arguments
                ArgumentPack args;
                args.add(i * 100 + j);
                args.add("Thread " + std::to_string(i));
                
                // Emit signal safely from this thread
                signalSource->emit("event", args);
                
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        });
    }
    
    // Wait for all threads to complete
    for (auto& thread : threads) {
        thread.join();
    }
}

ArgumentPack

The ArgumentPack class provides a type-safe container for passing arbitrary arguments with signals:

Using ArgumentPack
// Creating an ArgumentPack
ArgumentPack args;
args.add("filename.txt");
args.add(42);
args.add(3.14159);

// Direct creation with variadic constructor
ArgumentPack args2("filename.txt", 42, 3.14159);

// Retrieving values by index and type
std::string filename = args.get(0);
int count = args.get(1);
double value = args.get(2);

// Getting the number of arguments
size_t numArgs = args.size();

// Checking if empty
bool isEmpty = args.empty();

// Cloning an ArgumentPack (deep copy)
ArgumentPack copy = args.clone();
Note: Arguments are stored in the order they are added, and must be retrieved in the same order and with the correct type. The get<T>(index) method will throw a std::bad_cast exception if the requested type doesn't match the stored type.

Connection Management

The SignalSlot system provides a ConnectionHandle for managing signal-slot connections:

Managing Connections
// Get a connection handle when connecting to a signal
ConnectionHandle connection = object->connect("dataReceived", myCallback);

// Later, disconnect when no longer needed
connection->disconnect();

// Check if still connected
if (connection->connected()) {
    // ...
}

Connection handles use shared ownership to ensure connections are properly managed even when handlers go out of scope or are accessed from multiple threads.

Thread-Safe Best Practices

Thread-Safe Implementation Details

Key Thread Safety Mechanisms

Advanced Thread-Safe Features

Signal Emission Across Thread Boundaries

The SignalSlot system allows signals to be safely emitted across thread boundaries:

Cross-Thread Signal Emission
// Thread 1: Set up signals and connections
auto manager = std::make_shared();
manager->createSignal("taskCompleted");

// Thread 2: Emit signals
std::thread worker([manager]() {
    // Perform work...
    
    // Create result arguments
    ArgumentPack results;
    results.add(42);
    results.add("Task completed successfully");
    
    // Safely emit signal from worker thread
    manager->emit("taskCompleted", results);
});

Thread-Safe Connection Lifetimes

ConnectionHandles ensure connections are properly managed across threads:

Thread-Safe Connection Management
std::vector connections;
std::mutex connectionsMutex;

// Thread 1: Create connections
{
    std::lock_guard lock(connectionsMutex);
    connections.push_back(source->connect("event", handler1));
    connections.push_back(source->connect("event", handler2));
}

// Thread 2: Disconnect a specific connection
{
    std::lock_guard lock(connectionsMutex);
    if (!connections.empty()) {
        connections[0]->disconnect();
    }
}

Summary

The SignalSlot class provides a robust, thread-safe foundation for event-based communication in the Task Framework. By enabling loosely coupled interaction between components, even across thread boundaries, it promotes modular, maintainable code structures that can be easily extended and modified in concurrent applications.

This implementation balances flexibility, type safety, and thread safety, making it suitable for a wide range of concurrent application requirements. The consistent interface across the framework ensures that all components can participate in the signal-slot communication system with minimal integration effort.