© Carlos Oliveira 2021
C. OliveiraPractical C++20 Financial Programminghttps://doi.org/10.1007/978-1-4842-6834-6_17

17. Multithreading

Carlos Oliveira1  
(1)
Seattle, WA, USA
 

C++20 applications are used in contexts where computational performance has great importance. The need for performance is even more prominent in financial applications such as high-frequency trading, where the difference between profit and loss may be just a few microseconds away. In such cases where performance is a requirement, it is very important to take advantage of the resources made available in modern CPUs (central processing units). In particular, multicore processing is one of the main features provided by new CPUs, with the number of available cores constantly increasing along with the complexity of chips.

To benefit from multicore processors, however, C++ programmers need to learn a few parallel programming techniques that have rapidly become part of the C++ repertoire during the last decades. Using multiple processes is a possible way to explore this computational power. Multithreading, a method used to run several concurrent tasks inside the same process, is another technique that has the potential to take advantage of two or more cores at the same time.

In this chapter, you will see a few examples that explore multithreading strategies for C++ programmers. With the knowledge provided in this chapter, you will be able to take full advantage of existing multicore systems on your applications. While multithreading is a useful strategy to employ in today’s applications, it will become even more important in the future, as desktop and server manufacturers are expected to continue to add more cores to their processors.

In the traditional approach for multithreading, C++ programmers used libraries created to facilitate the access to the multithreading facilities provided by the operating system. A popular example is the pthreads library . However, the current standard C++20 provides another approach, whereby the use of threading is directly supported by the language, through templates in the <thread> header file. In this chapter, you will learn both ways. This is important because much of the existing multithreading code uses nonstandard libraries. However, new code should preferably use the templates provided by the standard library.

The following are some of the topics discussed in this chapter:
  • Using the pthreads library: pthreads is a standard library that can be used to create and maintain multiple threads in the same process. In a multicore machine, creating threads is one of the most common ways to explore the parallel abilities of current architectures. You will learn how to create applications that employ the pthreads library to achieve parallel computation.

  • Running algorithms in parallel threads: You will see how to decompose problems in separate threads and combine their results into a new solution. As an example, I present a modified parallel algorithm for the calculation of options probabilities.

  • Thread synchronization: The use of multiple threads introduces the problem of synchronizing resources. You will learn how to use synchronization primitives such as mutexes to guarantee that resources are accessed and modified by only one thread at each time.

  • STL threads: The new multithreading classes and templates provided by the latest releases of the C++ standard.

Creating Threads with the Pthreads Library

Create a C++ class that distributes its work through several processing threads using the standard pthreads library.

Solution

Multithreading is one of the software solutions that have been created to support parallel computation. A thread is a unit of processing that can be performed in parallel along with other parts of a program, so that two or more segments of a program can be executed concurrently. In a multicore machine, this means that the same program may efficiently use two or more cores to perform additional work. Depending on how the code is organized, the careful use of multithreading techniques provides a good opportunity to improve the throughput of the whole algorithm.

To use multithreading, however, support from the operating system is necessary. Since each operating system implements multithreading internally in its own way, it used to be the case that a multithreading application would be dependent on the operating system and architecture used. To avoid this problem, a standard pthreads (POSIX threads) library was proposed and adopted as part of POSIX. The pthreads library is available for multiple operating systems, including UNIX, Mac OS X, and even Windows (e.g., you can use the Cygwin libraries to access pthreads on Windows). Table 17-1 provides a quick summary of functions in the pthreads API (application programming interface) that are available for application developers.
Table 17-1

List of Commonly Used Functions in the Pthreads Library

Function name

Description

pthread_create

Creates a new thread

pthread_exit

Finishes an existing thread

pthread_join

Joins an existing thread, returning only after the thread exits

pthread_detach

Detaches from a thread

pthread_attr_init

Initializes an attribute data structure

pthread_attr_destroy

Destroys an attribute data structure

pthread_attr_setstacksize

Sets the size of the stack for a new thread

pthread_cancel

Cancels the thread execution

pthread_mutex_init

Initializes a mutex synchronization primitive

pthread_mutex_destroy

Destroys a mutex

pthread_mutex_lock

Locks a mutex

pthread_mutex_unlock

Unlocks an existing mutex

sem_init

Initializes a semaphore synchronization primitive

sem_destroy

Destroys a semaphore

sem_wait

Waits on a semaphore

While the pthreads library is written for compatibility with pure C programs, it can easily be used as part of C++ applications. It is simple to create a wrapper around threads created with pthreads so that they can be more easily accessible from C++ code. In this section, you will see a C++ code example for creating a simple multithreaded application using pthreads. The techniques used as well as the general concept of thread creation and synchronization can be used in your own programs.

To create a separate thread of execution within a program, you need to use the pthread_create function . It receives as parameters an identifier (integer value), a pointer to an attribute structure (which can be null if not used), a pointer to a function that will be executed by the thread, and a pointer to the arguments to the thread function. The function returns zero if no error happened; otherwise, it returns an integer error identifier.

After the pthread_create function is executed, the program starts another thread from the specified function. That thread is independent of the original program and may run in the same or in a separate core, if there is one available in the host machine as determined by the operating system’s thread scheduler. A thread can be terminated either by reaching the end of the thread function or by explicitly calling the pthread_exit function.

In this section, I show how to access the functionality provided by the pthreads library from a C++ class. For this purpose, I introduce the Thread class, which encapsulates the concept of a running thread. The goal of this class is to become a base class for concrete thread classes. The only member function that is required in each new subclass of Thread is run(), which determines the code that will be executed by the new thread.

Notable methods in the Thread class are the following:
  • start: Needs to be called to start the execution of the thread.

  • endThread: Can be called to terminate the current thread.

  • setJoinable: Determines if the thread can be joined by other threads.

  • join: Allows a caller to join this thread, in such a way that the caller will continue its execution only after the thread has terminated.

  • run: This member function needs to be implemented in each subclass and defines the code that will run in its own thread.

The Thread class uses a C function called thread_function and is defined in the Thread.cpp file:
void *thread_function(void *data)
{
    Thread *t = reinterpret_cast<Thread*>(data);
    t->run();
    return nullptr;
}

The signature of this function is determined in the pthreads library. The function is called as soon as a new thread is created. The main idea is that the data pointer passed to the function is in fact a pointer to a Thread object . Once it is retrieved using the reinterpret_cast operator , the object can be used to perform the run member function. Depending on the concrete subclass of Thread, the run method may do any task desired by the creator of the subclass. This is enough to guarantee that the code will run as a parallel thread.

Note

Remember that the reinterpret_cast operator can be used to convert between any two types in C++. Therefore, it is important to be careful when using this operator, since there is no type checking performed by the compiler once it is applied.

Other than that, the start() and endThread() functions use the corresponding pthread API functions to perform the creation of a new thread and to exit from an existing thread, respectively. This is how these functions are implemented:
void Thread::start()
{
    pthread_create(&m_data->m_thread, &m_data->m_attr, thread_function, this);
}
void Thread::endThread()
{
    pthread_exit(nullptr);
}

Complete Code

You can find the complete implementation for the Thread class in Listing 17-1.
//
//  Thread.h
#ifndef __FinancialSamples__Thread__
#define __FinancialSamples__Thread__
struct ThreadData;
class Thread {
public:
    Thread();
    virtual ~Thread();
private:
    Thread(const Thread &p); // no copy allowed
    Thread &operator=(const Thread &p); // no assignment allowed
public:
    virtual void run() = 0;
    void start();
    void endThread();
    void setJoinable(bool yes);
    void join();
private:
    ThreadData *m_data;
    bool m_joinable;
};
#endif /* defined(__FinancialSamples__Thread__) */
//
//  Thread.cpp
#include "Thread.h"
#include <pthread.h>
#include <iostream>
using std::cout;
using std::endl;
struct ThreadData {
    pthread_t m_thread;
    pthread_attr_t m_attr;
};
namespace {
    void *thread_function(void *data)
    {
        Thread *t = reinterpret_cast<Thread*>(data);
        t->run();
        return nullptr;
    }
}
Thread::Thread()
: m_data(new ThreadData),
  m_joinable(false)
{
    pthread_attr_init(&m_data->m_attr);
}
Thread::~Thread()
{
    if (m_data)
    {
        delete m_data;
    }
}
void Thread::start()
{
    pthread_create(&m_data->m_thread, &m_data->m_attr, thread_function, this);
}
void Thread::endThread()
{
    pthread_exit(nullptr);
}
void Thread::setJoinable(bool yes)
{
    pthread_attr_setdetachstate(&m_data->m_attr,
 yes ? PTHREAD_CREATE_JOINABLE : PTHREAD_CREATE_DETACHED);
    m_joinable = yes;
}
void Thread::run()
{
    cout << " no concrete implementation provided " << endl;
}
void Thread::join()
{
    if (!m_joinable)
    {
        cout << " thread cannot be joined " << endl;
    }
    else
    {
        void *result;
        pthread_join(m_data->m_thread, &result);
    }
}
// --- sample implementation
class TestThread : public Thread {
public:
    virtual void run();
};
void TestThread::run()
{
    cout << " this is a test implementation " << endl;
    endThread();
}
int main()
{
    Thread *myThread = new TestThread;
    myThread->setJoinable(true);
    myThread->start();
    myThread->join();
    return 0;
}
Listing 17-1

Thread Class and a Sample Implementation

Running the Code

The code displayed in Listing 17-1 can be built using any standards-compliant compiler, such as gcc, llvm, or Visual Studio. Just remember to add a link line including the pthreads library. The following is a command line used to build the sample application (tested on Mac OS X):
$ gcc –o threadTest Thread.cpp -lpthreads
$ ./threadTest
  this is a test implementation

Calculating Options Probabilities in Parallel

Create a multiprocessing version of the class that calculates options probabilities. Use the pthreads library to distribute work among several threads.

Solution

The use of parallel processing techniques is highly indicated for problems that require massive amounts of computation. This is especially true when the problem can be easily decomposed, in which case it becomes a matter of distributing the right amount of work to each thread and waiting for the results.

A good example of such a process is a Monte Carlo-based algorithm. The simulation process can run in any number of threads, and their results can be combined easily into a single number. This is the case, for example, of calculating options probabilities. As you saw in Chapter 14, Monte Carlo techniques are effective for the simulation of options probabilities. At each step, you just need to simulate a new random walk and use that information to improve the current estimate of the probability.

To adapt the Monte Carlo algorithm to the determination of options probabilities, the first step is to correctly define the way in which the problem will be decomposed. This is easy to do here, because each loop of the computation is independent of the other. In this case, you can do this by telling each thread to run a certain number of iterations of the Monte Carlo method. At the end, you can combine the results found by each thread and calculate the final result as the average of the values returned by all threads.

The algorithm just described is implemented in the ParallelOptionsProbabilities class . The class is an outer shell that invokes several threads to run the desired algorithm. The real work is done in a class derived from Thread, and called RandomWalkThread. As any other subclass of Thread, it needs to implement the run() member function, which is called from the separate thread. Inside RandomWalkThread, you will find a member variable, m_result, which stores the output of the Monte Carlo process. After the thread is finished, this member variable can be used to retrieve the final value of the computation.

The run member function is very similar to the code you already saw in the OptionsProbabilities class. The main difference is that the output is stored in the m_result member variable. The work of RandomWalkThread objects is orchestrated inside the ParallelOptionsProbabilities class. The important member function for the job is probFinishAboveStrike.
double ParallelOptionsProbabilities::probFinishAboveStrike()
{
    const int numThreads = 20;
    vector<RandomWalkThread*> threads(numThreads);
    for (int i=0; i<numThreads; ++i)
    {
        threads[i] = new RandomWalkThread(m_numSteps, m_step, m_strikePrice);
        threads[i]->setJoinable(true);
        threads[i]->start();
    }
    for (int i=0; i<numThreads; ++i)
    {
        threads[i]->join();
    }
    double nAbove = 0;
    for (int i=0; i<numThreads; ++i)
    {
        nAbove += threads[i]->result();
        delete threads[i];
    }
    return nAbove/(double)(numThreads);
}

At the beginning of the member function, several threads are created and added to the threads vector. You need to define these threads as joinable, so that it is possible to wait on the result of each thread. The next step is to start the threads so that each of them can perform the desired computations. Then, the second loop is used to join the already created threads. By doing this, the main thread can wait while the computation is being performed in parallel. When all threads are finished, the main thread will be resumed as a result of the call to join(). Finally, you can store the data returned by each thread using the result() member function. The thread objects may be deleted at this time to avoid memory leaks. In the last line of the probFinishAboveStrike member function, you can see how the calculated data can be combined. In this case, it is enough to return the sum of values above the strike prices and divide that value by the number of threads used.

Complete Code

Listing 17-2 displays the ParallelRandomWalk class . There is a sample main() function that can be used for testing, as can be seen at the end of the listing.
//
//
//  ParallelRandomWalk.h
#ifndef __FinancialSamples__ParallelRandomWalk__
#define __FinancialSamples__ParallelRandomWalk__
class ParallelOptionsProbabilities  {
public:
    ParallelOptionsProbabilities(int size, double strike, double sigma);
    ParallelOptionsProbabilities(const ParallelOptionsProbabilities &p);
    ~ParallelOptionsProbabilities();
    ParallelOptionsProbabilities &operator=(const ParallelOptionsProbabilities &p);
    double probFinishAboveStrike();
private:
    int m_numSteps;       // number of steps
    double m_step;        // size of each step (in percentage)
    double m_strikePrice; // starting price
};
#endif /* defined(__FinancialSamples__ParallelRandomWalk__) */
//
//  ParallelOptionsProbabilities.cpp
#include "ParallelOptionsProbabilities.h"
#include "Thread.h"
#include <pthread.h>
#include <cstdlib>
#include <vector>
#include <boost/random/normal_distribution.hpp>
#include <boost/random.hpp>
using std::vector;
using std::cout;
using std::endl;
static boost::rand48 random_generator;
using std::vector;
/// ---
class RandomWalkThread : public Thread {
public:
    RandomWalkThread(int num_steps, double sigma, double startPrice);
    ~RandomWalkThread();
    virtual void run();
    double gaussianValue(double mean, double sigma);
    double getLastPriceOfWalk();
    double result();
private:
    int m_numberOfSteps;     // number of steps
    double m_sigma;          // size of each step (in percentage)
    double m_startingPrice;  // starting price
    double m_result;
};
RandomWalkThread::RandomWalkThread(int numSteps, double sigma, double startingPrice)
: m_numberOfSteps(numSteps),
  m_sigma(sigma),
  m_startingPrice(startingPrice)
{
}
RandomWalkThread::~RandomWalkThread()
{
}
double RandomWalkThread::gaussianValue(double mean, double sigma)
{
    boost::random::normal_distribution<> distrib(mean, sigma);
    return distrib(random_generator);
}
double RandomWalkThread::result()
{
    return m_result;
}
double RandomWalkThread::getLastPriceOfWalk()
{
    double prev = m_startingPrice;
    for (int i=0; i<m_numberOfSteps; ++i)
    {
        double stepSize = gaussianValue(0, m_sigma);
        int r = rand() % 2;
        double val = prev;
        if (r == 0) val += (stepSize * val);
        else val -= (stepSize * val);
        prev = val;
    }
    return prev;
}
void RandomWalkThread::run()
{
    cout << " running thread " << endl;
    int nAbove = 0;
    for (int i=0; i<m_numberOfSteps; ++i)
    {
        double val = getLastPriceOfWalk();
        if (val >= m_startingPrice)
        {
            nAbove++;
        }
    }
    m_result = nAbove/(double)m_numberOfSteps;
}
// ---
ParallelOptionsProbabilities::ParallelOptionsProbabilities(int size, double start, double step)
: m_numSteps(size),
  m_strikePrice(start),
  m_step(step)
{
}
ParallelOptionsProbabilities::ParallelOptionsProbabilities(const ParallelOptionsProbabilities &p)
: m_numSteps(p.m_numSteps),
  m_strikePrice(p.m_strikePrice),
  m_step(p.m_step)
{
}
ParallelOptionsProbabilities::~ParallelOptionsProbabilities()
{
}
ParallelOptionsProbabilities &ParallelOptionsProbabilities::operator=(const ParallelOptionsProbabilities &p)
{
    if (this != &p)
    {
        m_numSteps = p.m_numSteps;
        m_strikePrice = p.m_strikePrice;
        m_step = p.m_step;
    }
    return *this;
}
double ParallelOptionsProbabilities::probFinishAboveStrike()
{
    const int numThreads = 20;
    vector<RandomWalkThread*> threads(numThreads);
    for (int i=0; i<numThreads; ++i)
    {
        threads[i] = new RandomWalkThread(m_numSteps, m_step, m_strikePrice);
        threads[i]->setJoinable(true);
        threads[i]->start();
    }
    for (int i=0; i<numThreads; ++i)
    {
        threads[i]->join();
    }
    double nAbove = 0;
    for (int i=0; i<numThreads; ++i)
    {
        nAbove += threads[i]->result();
        delete threads[i];
    }
    return nAbove/(double)(numThreads);
}
int main()
{
    ParallelOptionsProbabilities rw(100, 50.0, 52.0);
    double r = rw.probFinishAboveStrike();
    cout << " result is " << r << endl;
    return 0;
}
Listing 17-2

Class ParallelRandomWalk

Running the Code

I have compiled and executed the code displayed in Listing 17-2 using the gcc compiler on a Mac OS X machine. Any standards-compliant compiler can be used for this purpose. The following is a sample of the expected output:
./parallelOptProb
 running thread
 running thread
 ...
 running thread
result is 0.487

Using Mutexes to Prevent Unsynchronized Access

In this section, we will write a C++ class that implements a parallel algorithm where mutexes are used to synchronize shared data.

Solution

Multithreading is a convenient way to distribute computational work into two or more processor cores, which can lead to an increase in performance for the whole application. However, while multithreading has numerous advantages, it also adds to the complexity of the software design. For example, one of the problems that need to be solved in multithreading architectures is the access to resources shared between threads. If a variable in memory is used in two or more threads, its access needs to be synchronized so that separate threads will not try to change values concurrently, for example.

Once a new thread has been created, it is necessary to manage it, using mechanisms provided by the pthread library. In the simplest case, the new thread is independent and does not need to be synchronized with the original (parent) thread. More commonly, however, it is necessary to perform synchronization between separate threads that use the same data. The greater the need for synchronization, the greater the amount of work spent on managing the shared data.

A section of code where two or more threads can access a shared resource is called a critical section. The critical sections are the areas of the code where shared resources need to be protected, in order to avoid conflicts.

To avoid the conflicts inherent to the existence of critical sections, most multithreading APIs provide primitives that can be used to implement synchronized operations. Such operations have proved effective in enabling resource sharing between threads. There are a number of such primitives, such as semaphores, mutexes, and messages, among others. The pthreads library provides direct support for some of the most common of such mechanisms, including mutexes, which can be used to guarantee that only one thread is able to access a particular critical section.

A mutex is a synchronization mechanism that can be used to coordinate the work of two or more threads. The mutex state is used to determine if a thread has permission to operate on a particular resource, such as a variable in memory. When a thread tries to access the value of the mutex, two things can happen: if the mutex state indicates that the critical section is available, then the thread can directly proceed to the critical section. However, if the mutex state indicates a busy state, the thread making the request stops its execution and is sent to a waiting area created by the operating system. Operation will resume only when the resource has been made available by other threads. All this waiting and resuming activity is coordinated by the operating system.

Mutexes are implemented in the pthreads API and have the type thread_mutex_t. A new mutex can be created using the function pthread_mutex_init and destroyed using the function pthread_mutex_destroy.

A mutex needs to be acquired and locked when a shared resource is about to be used. This guarantees that the mutex will be available for only one thread at a time. This is done using the function pthread_mutex_lock . This function will automatically interrupt the thread if the mutex is not available and force the thread to wait until the mutex has been released. You can also try to access the mutex without a forced wait using pthread_mutex_trylock. This will return an error code in case the mutex is currently not available, and you will be free to try it later. Finally, once a mutex has been acquired, you need to unlock the mutex at the end of the synchronized operation. This is necessary to make sure that other threads can enter the critical section and use the recently released resource. You can unlock a mutex using the function pthread_mutex_unlock .

While theoretical proof of the effectiveness of the mutex can be complex, its use is very simple. In the coding example in Listing 17-3, you will have a class called Mutex that encapsulates the concept of a mutex synchronization operation. There are two main functions provided by this class: lock and unlock. The first member function is called at the beginning of a critical section. The second important member function in the class is unlock, which should be called at the end of a critical section. The Mutex class is also responsible for initializing the pthread mutex at the constructor with pthread_mutex_init and destroying it at the destructor with pthread_mutex_destroy.

The second class used to encapsulate the mutex concept is MutexAccess . This class is responsible for guaranteeing that each access to the mutex is composed of a pair of calls to the lock() and unlock() member functions of Mutex. The lock() member function is directly called in the constructor, and unlock() is automatically called in the destructor of MutexAccess . Therefore, if the critical section ends right at the end of the scope where the MutexAccess object is declared, you don’t need to worry about unlocking it, since the RAII idiom guarantees that the mutex will be automatically unlocked when the destructor is called.

In the MutexTestThread , we have an example of using the Mutex class inside a thread. The task demonstrated is really simple, but it illustrates how the mutex can be used to provide synchronization of access to shared resources. Here, the shared resource is the variable result, of double type. This variable is used to hold the desired calculation; however, it is being accessed in all threads in the application. In order to synchronize access to this variable, you need to use a mutex. An object of the class MutexAccess can be instantiated, resulting in the mutex (named m_globalMutex) being locked. After the lock has been acquired, you can now safely check the value and make changes to the reference variable. Finally, at the end of the run() member function, the lock will be released automatically.

Complete Code

You can view the complete code for the Mutex and MutexAccess classes in Listing 17-3. An example of their use is also shown in class MutexTestThread.
//
//  Mutex.h
#ifndef __FinancialSamples__Mutex__
#define __FinancialSamples__Mutex__
struct MutexData;
class Mutex {
public:
    Mutex();
    ~Mutex();
    void lock();
    void unlock();
private:
    Mutex(const Mutex &p);  // copy not allowed
    Mutex &operator=(const Mutex &p);  // assignment not allowed
    MutexData *m_data;
};
class MutexAccess {
public:
    MutexAccess(Mutex &m);
    ~MutexAccess();
private:
    MutexAccess &operator=(const MutexAccess &p);
    MutexAccess(const MutexAccess &p);
    Mutex &m_mutex;
};
#endif /* defined(__FinancialSamples__Mutex__) */
//
//  Mutex.cpp
#include "Mutex.h"
#include "Thread.h"
#include <pthread.h>
#include <cstdlib>
#include <vector>
#include <iostream>
using std::vector;
using std::cout;
using std::endl;
struct MutexData {
    pthread_mutex_t m_mutex;
};
Mutex::Mutex()
: m_data(new MutexData)
{
    pthread_mutex_init(&m_data->m_mutex, NULL);
}
Mutex::~Mutex()
{
    if (m_data)
    {
        pthread_mutex_destroy(&m_data->m_mutex);
        delete m_data;
    }
}
void Mutex::lock()
{
    pthread_mutex_lock(&m_data->m_mutex);
}
void Mutex::unlock()
{
    pthread_mutex_unlock(&m_data->m_mutex);
}
/// ----
MutexAccess::MutexAccess(Mutex &m)
: m_mutex(m)
{
    m_mutex.lock();
}
MutexAccess::~MutexAccess()
{
    m_mutex.unlock();
}
/// ----
class MutexTestThread  : public Thread {
public:
    MutexTestThread(double &result, double incVal);
    ~MutexTestThread();
    void run();
private:
    double &m_result;
    double m_incValue;
    static Mutex m_globalMutex;
};
Mutex MutexTestThread::m_globalMutex;  // global mutex is static
MutexTestThread::MutexTestThread(double &result, double incVal)
: m_result(result),
  m_incValue(incVal)
{
}
MutexTestThread::~MutexTestThread()
{
}
void MutexTestThread::run()
{
    MutexAccess maccess(m_globalMutex);  // mutex is locked here
    cout << " accessing data " << endl; cout.flush();
    if (m_result > m_incValue)
    {
        m_result -= m_incValue;
    }
    else
    {
        m_incValue += m_incValue;
    }
    // mutex is automatically unlocked
}
int main()
{
    int nThreads = 10;
    vector<Thread*> threads(nThreads);
    double price = rand() % 25;
    for (int i=0; i<nThreads; ++i)
    {
        threads[i] = new MutexTestThread(price, (double)(rand() % 10));
        threads[i]->setJoinable(true);
        threads[i]->start();
    }
    for (int i=0; i<nThreads; ++i)
    {
        threads[i]->join();
    }
    cout << " final price is " << price << endl;
    return 0;
}
Listing 17-3

The Mutex Class

Running the Code

You can compile this code using any standard C++ compiler. I performed the test on a machine running the Mac OS X operating system. The following is a display of sample results:
accessing data
accessing data   ...
accessing data
accessing data
accessing data
final price is 2

Creating Threads Using the Standard Library

In the previous section, you learned how to create multithreaded programs using the pthreads library . In C++20, it is also possible to create multithreaded code using the standard library. The support is provided through the <thread> header file.

To make simple multithreaded programs using the STL, it is not necessary to create new classes or objects. The class std::thread already has the ability to perform multithreaded operations using as input a function, a lambda, or a functional object.

Consider the following example:
#include <thread>
#include <iostream>
#include <vector>
void compute_max(const std::vector<double> &values)
{
   auto total = 0.0;
   for (auto v : values)
   {
      total += v;
   }
   std::cout << " total: " << total << std::endl;
}
int main()
{
   std::vector<double> v = {0, 5, 3, 2, 5, 3};
   std::thread first_tread(compute_max, v);
   first_tread.join();
   return 0;
}

Here, we define a simple function called compute_max, which receives as parameter a vector of double numbers. This function could be any type of operation that takes a long time and that we would like to move to a separate thread. To create a new thread using this function, we just need to use the std::thread class in the <thread> header file.

The std::thread class takes as parameters the name of the function (or functional object) you want to use, along with zero or more parameters that will be passed to that function. In the previous example, we have the vector named v as the single parameter. This could be expanded to other parameters if required by the function compute_max.

Finally, the first_thread object calls the join() method, to indicate that the main function will join the execution of that thread, until it is complete. If we didn’t want to stop until the thread is completed, we could have used instead the detach() method, which allows the thread to run independently while the current function continues its operation.

Conclusion

The constant development of multicore processors and architectures has greatly expanded the computational capacity of modern machines. However, to explore such multicore architectures, it is necessary to change the way you program. Modern high-performance programming has an increased focus on multiprocessing techniques, which allow applications to access more than one core and as result improve the performance of the system.

Multithreading is a programming technique that allows more than one thread of execution per process. If the machine has more than one processor, multithreading allows you to access these processing cores to perform additional work. In this chapter, you learned how to create, terminate, and manage threads using the standard pthreads library.

In the first programming example, you learned about the pthreads library and how it can be used to create new threads. You saw how to design a C++ class to encapsulate the pthreads function calls. Using pthreads, you can simplify your multithreading applications, as it abstracts away system-dependent APIs for multithreading.

Next, you learned how to apply pthreads to a common problem on options. You saw that, for some problems, it is easy to distribute the necessary work into separate units of computation. Using C++, you can encapsulate such code segments into different objects.

In the next section, you learned about synchronization primitives and how they are implemented using the pthreads library. I introduced a class that can be used to model the operation of a mutex. You can readily apply the Mutex class to other financial programming projects.

Finally, I also explained how the new C++ standard C++20 provides direct support for threads without the use of a separate library like pthreads. Thus, for new code, it is possible to simplify the applications and rely on the STL. While much of the existing multithreading code still uses libraries like pthread, it is important to learn how to do this using the standard and use it in new projects.

With this chapter, I have completed a general presentation of technical tools used to create high-performance financial applications in C++. I hope you have enjoyed learning about the features of C++ and how they can be applied to the solution of common problems in the financial industry.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.103.202