Reducing Latency and Increasing Throughput in Trading Systems with Multithreading ( Part 1 ) || C++

Fundamentals of Multithreading

Concurrency

Execution of multiple instruction sequences at the same time. Concurrency can be achieved through various mechanisms, such as multi-threading, multiprocessing, or asynchronous programming.

For example, Let's suppose you are writing code in an Integrated Development Environment (IDE). In this situation, one thread is occupied with taking input, while another is simultaneously compiling and indicating compilation errors.

Thread

A thread refers to the smallest unit of execution within a process. A process can have multiple threads. These threads within a process share the same resources, such as memory space and file handles, but they run independently.

Creating Threads

We need to create a std::thread object, the class is defined in <thread>. The constructor starts a new execution thread. The Parent thread will continue its own execution. std::thread's constructor takes and callable object which is called Entry Point. The execution thread will invoke this function.

#include <iostream>
#include <thread>

void myFun()
{
    std::cout << "Hello World" << std::endl;
}

int main()
{
    std::thread t1(myFun); // callable object { Entry Point }
    return 0;
}

Note: This program will be terminated.

Zombie thread and Join()

When you run the above program, it will terminate. This termination occurs because, as the main thread goes out of scope the thread object destructor is called. In the destructor, std::terminate() is invoked, leading to the termination of all threads, including the main thread, even if its child threads are still running. These threads, without an owner, are referred to as Zombie threads.

To avoid such premature termination, we use std::join(). This function blocks the calling thread (in this case, the main thread) until the thread represented by the thread object completes its execution.

int main()
{
    std::thread t1(myFun); // callable object { Entry Point }
    t1.join(); // wait to t1 to finish
    return 0;
}

Passing Parameters

In C++, when creating a thread with the std::thread class, you can pass arguments to the thread function directly after the function name in the std::thread constructor. You can use various callable objects with threads, and accordingly, several ways to pass parameters to these objects. Here are some examples to illustrate these concepts:

Lambda Functions: Lambda expressions are a concise way to define anonymous functions directly within the argument list of the std::thread constructor. They are especially useful for passing parameters when you want to execute a simple function that won't be reused elsewhere. However, it's essential to use them judiciously, as their in-line nature might lead to assumptions about their scope and lifetime, especially in complex applications. Here's an example,

int main()
{
    std::string message = "multiplication of a and b is : ";
    std::thread t1( 
        // first argument is the as lambda expression.
        [&message] (int a, int b) {
            std::cout << message << a * b << std::endl;
        },
        // The remaining arguments are passed to the lambda expression.
        2, 3
    );
    t1.join();
    return 0;
}

Member Functions: To pass a class member function as a thread function. The first parameter should be a pointer to the member function. Following that, provide a pointer to the object, since the member function implicitly takes the this pointer as its first argument. Then, include any additional arguments that the function requires. Here's an example:

class Calculater 
{
    public:
    void sum(int a, int b)
    {
        std::cout << "sum of a and b is : " << a + b << std::endl;
    }
};

int main()
{
    Calculater obj;
    std::thread t1(&Calculater::sum, &obj, 5, 10);
    t1.join();
    return 0;
}

R-values and L-values: Passing parameters to a function that accepts an r-value reference in C++ threads can be straightforward. However, special consideration is required when you wish to pass an l-value as an r-value reference. In such cases, you must explicitly cast the l-value to an r-value using std::move(). This action signifies a transfer of ownership to the receiving function, implying that the original variable should not be used after the transfer, as its state is now indeterminate.

For passing variables as references, use std::ref() for non-const references and std::cref() for const references. To use these functions you have to include a functional library.

#include <functional> // For std::ref and std::cref
void fun(int &&a)
{
    std::cout << "The value of a is: " << a << std::endl;
}

void demo(int &x, const int &y)
{
    std::cout << "addressess of x and y are in thread : " << &x << " " << &y << std::endl;
}

int main()
{
    // Directly passing an r-value is straightforward
    std::thread t1(fun, 10);
    t1.join();

    // To pass an l-value as an r-value, use std::move().
    // Do this only if you won't need the variable afterward,
    // as ownership of its contents is transferred.
    int a = 10;
    std::thread t2(fun, std::move(a));
    t2.join();

    // l-values by refrence and const refrence.
    int x = 0, y = 1;
    std::thread t3(demo, ref(x), cref(y));
    t3.join();

    return 0;
}

Thread Functions

Pausing Thread: We can pause a thread's execution by invoking the function sleep_for, which accepts an input of type std::chrono::duration. This functionality is not limited to child threads; the main thread can also be paused in a similar manner.

Here are examples demonstrating how to pause a thread in C++11 and C++14:

void fun11()
{
    cout << "Thread is function fun11 " << endl;
    this_thread::sleep_for(chrono::seconds(2)); // c++ 11
}

void fun14()
{
    cout << "Thread is function fun14 " << endl;
    this_thread::sleep_for(2s); // c++ 14
}

int main()
{
    thread t1(fun11);
    t1.join();

    thread t2(fun14);
    t2.join();
    return 0;
}

Thread Id: Each execution thread in C++ has a unique thread identifier (ID). This ID can be obtained using the get_id function, which is crucial for distinguishing between different threads within a program. Notably, a new thread may receive the ID of a previously completed thread. There are two common ways to retrieve a thread's ID:

  • std::this_thread::get_id(); returns the ID of the current thread.

  • t1.get_id(); returns the ID associated with a thread object.

Here's a concise example to illustrate both methods:

void fun()
{
    std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;
}

int main()
{
    std::thread t1(fun);
    std::cout << "Thread object t1's ID: " << t1.get_id() << std::endl;
    t1.join();
    return 0;
}

Detach: When an execution thread is detached, it is no longer associated with the std::thread object. This means the destructor of the std::thread object will not call std::terminate(). Additionally, after a thread has been detached, it cannot be joined again. Attempting to join a detached thread results in undefined behavior. The resources of a detached thread are automatically released back to the system when the thread's execution finishes.

void task() {
    // Simulate work.
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Task completed" << std::endl;
}

int main() {
    std::thread t(task);
    t.detach();  // Detach the thread to allow it to execute independently.
    return 0; // The main function completes, but the detached thread may still be running.
}

It's crucial to ensure that the detached thread does not access any resources that may be destroyed or go out of scope while it is still running.

Exception Handling

When an exception is thrown within a try-catch block, the destructor of all local objects within that try block is indeed called. This process is called stack unwinding.

So, when an exception is thrown, the program starts unwinding the call stack, looking for a matching catch block. As it unwinds, the destructors of local objects on the stack are called. If a catch block is found that can handle the exception, the corresponding catch block is executed. If the catch block doesn't handle the exception or if there is no matching catch block within a thread, std::terminate() is called, and the program will likely end abruptly

class human
{
public:
    human()
    {
        cout << "Human constructor called." << endl;
    }

    ~human()
    {
        cout << "Human destructor is called." << endl;
    }
};

void task()
{
    cout << "Task function is called." << endl;
    try {
        human obj;
        throw runtime_error("Example exception");
    } catch(const std::exception& e) {
        cout << "Exception caught: " << e.what() << endl;
    }
}

int main() {

    thread t(task);
    t.join();

    return 0;
}

Managing threads objects

For managing thread objects, it's beneficial to use programming idioms like RAII (Resource Acquisition Is Initialization). In this approach, you encapsulate a thread object as a class member and ensure it is properly joined in the destructor, while also checking if it is joinable before attempting to join. This method helps ensure that resources are properly released and the thread is not left hanging.

#include <iostream>
#include <thread>

class ThreadWrapper {
public:
    ThreadWrapper(std::thread t) : t_(std::move(t)) {
        if (!t_.joinable()) {
            throw std::logic_error("No thread or non-joinable thread");
        }
    }

    ~ThreadWrapper() {
        if (t_.joinable()) {
            t_.join();
        }
    }

    // Delete the copy constructor and copy assignment operator
    ThreadWrapper(const ThreadWrapper&) = delete;
    ThreadWrapper& operator=(const ThreadWrapper&) = delete;

private:
    std::thread t_;
};

void task() {
    // Your thread task here
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread task completed\n";
}

int main() {
    std::thread t(task);
    ThreadWrapper tw(std::move(t));
    // The thread will be automatically joined at the end of the scope
    return 0;
}

C++20 and jthreads: If your system supports C++20, you can use std::jthread. This class internally utilizes the RAII concept for managing thread objects. std::jthread automatically joins upon destruction, simplifying thread management and making code safer by reducing the risk of forgetting to join a thread.

Conclusion

we've delved into the core concepts that underpin effective concurrent programming in C++. We started with the basics of concurrency and threads, illustrating how these elements serve as the foundation for building responsive and efficient applications.

We examined the practical aspects of thread creation in C++, from launching simple functions in separate threads to more complex scenarios involving class member functions and parameter passing. The nuances of managing thread lifecycles, such as joining and detaching threads, were highlighted to prevent common pitfalls like zombie threads or resource leaks.

As we wrap up this segment, it's clear that multithreading offers a vast potential for optimizing trading systems. Still, it also introduces complexity and demands a careful, informed approach to programming.

Stay tuned for Part 2, where we will explore the intricacies of synchronization, locking mechanisms, and the concurrency utilities introduced in C++11 and beyond, further expanding our toolkit for developing high-performance trading applications.

You can connect with me on:

Linkedin: https://www.linkedin.com/in/sahil-wadhwa0/