Thread Safety Guide#

Understanding thread safety requirements is critical for using the OUCH 5.0 library correctly.

Key Principle#

The Session class is not thread-safe. Most methods must be called from the thread that runs the IOContext’s event loop. However, some operations (constructor, destructor, and async operations) can be called from any thread.

Safe Operations#

Operations That Can Be Called from Any Thread#

These operations can be called from any thread:

  • Session() constructor - Can be called from any thread, but Session must be used only from IOContext’s thread after construction

  • ~Session() destructor - Safe to call from any thread

  • async_login() - Asynchronous login

  • async_start_with_listeners() - Asynchronous login with listeners

  • async_terminate() - Asynchronous termination

  • IOContext::post() - Post work to IOContext thread

Operations That Must Be Called from IOContext Thread#

These operations must be called from the IOContext’s thread:

  • send_message() - Send messages to exchange

  • get_user_refnum_generator() - Access UserRefNum generator

  • get_message_buffer() - Access message buffer

  • attach_listener() / detach_listener() - Manage listeners

Operations That Must NOT Be Called from IOContext Thread#

These operations must NOT be called from the IOContext’s thread (will cause deadlock):

  • login() - Synchronous login (blocking). Use async_login() or call from a non-IOContext thread

  • start_with_listeners() - Synchronous login with listeners. Use async_start_with_listeners() or call from a non-IOContext thread

Sending Messages from Another Thread#

To send messages from a thread other than the IOContext’s thread, use IOContext::post():

// From main thread or other thread
io_ctx->post([session = session.get(), order]() {
    // Now on IOContext's thread - safe to use Session
    EnterOrder order_copy = order;
    session->send_message(&order_copy);  // Safe
});

Async Operations#

Async operations can be called from any thread, but their callbacks run on the IOContext’s thread:

// Can be called from any thread
session->async_login("", 0, std::chrono::milliseconds(2000),
    [](std::uint64_t seq, const std::error_code& ec) {
        // Callback runs on IOContext's thread
        if (!ec) {
            // Safe to use session here
            std::cout << "Logged in with sequence: " << seq << std::endl;
        }
    });

UserRefNum Generator#

The UserRefNumGenerator is not thread-safe. It must be used from the IOContext’s thread:

// From IOContext's thread
auto& refnum_gen = session.get_user_refnum_generator();
UInt32 user_ref_num = refnum_gen.next();  // Safe

To generate UserRefNum from another thread:

// From another thread
io_ctx->post([&session]() {
    auto& refnum_gen = session.get_user_refnum_generator();
    UInt32 user_ref_num = refnum_gen.next();  // Safe: now on IOContext's thread
    // Use user_ref_num...
});

Message Handlers#

Message handlers (listeners) are always called on the IOContext’s thread, so it’s safe to use session operations within handlers:

void on_order_accepted(Session* s, std::uint64_t seq, const OrderAccepted* msg) {
    // This handler runs on IOContext's thread
    // Safe to use session operations
    
    if (auto* buffer = s->get_message_buffer()) {
        buffer->mark_acknowledged(msg->get_user_ref_num());  // Safe
    }
    
    // Safe to send messages
    CancelOrderRequest cancel;
    cancel.set_user_ref_num(msg->get_user_ref_num());
    cancel.set_quantity(0);
    s->send_message(&cancel);  // Safe
}

Best Practices#

  1. Always use IOContext::post() when calling send_message() from other threads

  2. Use async operations (async_login, async_terminate) for non-blocking code

  3. Access UserRefNum generator only from IOContext’s thread (or use post())

  4. Handle exceptions in handlers - wrap handler code in try-catch blocks

  5. Configure CPU affinity - Pin threads to specific cores for performance-critical applications

  6. Avoid blocking operations - Never perform blocking operations in message handlers

  7. Attach listeners from IOContext thread - While not strictly required, attach listeners from the IOContext’s thread to avoid potential race conditions

Example: Multi-Threaded Trading Application#

// Main thread
auto io_ctx = std::make_shared<IOContext>(
    IOContext::Settings{"ouch5", 1, false}
);
io_ctx->start();

auto session = std::make_unique<Session>(io_ctx, settings);

// Trading logic thread (different from IOContext thread)
std::thread trading_thread([&]() {
    // async_login can be called directly from any thread
    session->async_login("", 0, [](std::uint64_t seq, std::error_code ec) {
        // Callback runs on ouch5_ctx's thread
    });
    
    while (running) {
        // Generate order
        Order order = generate_order();
        
        // send_message() requires IOContext's thread, so use post()
        io_ctx->post([session = session.get(), order]() {
            // Now on IOContext's thread - safe to use Session
            auto& refnum_gen = session->get_user_refnum_generator();
            UInt32 user_ref_num = refnum_gen.next();
            
            EnterOrder msg;
            msg.set_user_ref_num(user_ref_num);
            // ... set fields from order ...
            
            session->send_message(&msg);  // Safe: we're on the correct thread
        });
        
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
});

// Keep running
trading_thread.join();