The C++ Programming Language – Part 5

This post will be mostly for personal reference as I go through Bjarne Stroustrop’s “The C++ Programming Language” 4th edition textbook. Some of the notes will appear random.

Chapter 5 – Concurrency and Utilities (continued)

Threads sometimes need to wait on events to do their work. For example, threads can wait for time to pass, wait for other threads to fill data buffers, pretty much anything you can think of. The classic example is the producer/consumer model. One thread processes data from a shared queue, while the other thread puts data into the shared queue. All operations on the shared queue are protected by a unique lock, making sure that only one thread can use the queue at a time. In this example, there is also a condition variable that would indicate to the processing thread that data is ready in the queue. Condition variables have a wait() function which takes a mutex and a Predicate. A C++ Predicate is “a function object that takes a single iterator argument that is dereferenced and used to return a value testable as a bool.” In the example below, the consumer thread will not try to acquire the mutex until the predicate returns true aka the queue is not empty.

// g++ -pthread -std=c++11 -o cond_var cond_var.cpp
#include <thread>
#include <iostream>
#include <mutex>
#include <queue>
#include <cstdbool>
#include <chrono>
#include <condition_variable>

using namespace std;

// globals to allow threads to work together
queue<int> mqueue;
condition_variable mcond;
mutex mmutex;

void producer()
{
    for (int i = 100; i < 105; i++) {
        // Acquire the lock.
        unique_lock<mutex> lck {mmutex};
        mqueue.push(i);
        cout << "producer - Pushed data to shared queue." << endl;
        // Unblock waiting threads.
        mcond.notify_one();
        cout << "producer - Notifying consumer and sleeping." << endl;
        // Scoped unlocking wasn't working properly for me, so I 
        // explicity unlock before looping.
        lck.unlock();
        this_thread::sleep_for( chrono::milliseconds(1000) );
    }
}

void consumer()
{
    while(true) {
        // Acquire the lock. Example of RAII.
        unique_lock<mutex> lck {mmutex};
        // Waiting on condition variable releases the mutex and attempts to 
        // reacquire once the Predicate returns true.
        mcond.wait(lck, []() { return !mqueue.empty(); });
        auto m = mqueue.front();
        mqueue.pop();
        lck.unlock();
        cout << "consumer - Got " << m << " from shared queue." << endl;
        cout << "consumer - Waiting for notify." << endl;
    }
}

int main(int argc, char *argv[])
{
    thread prod(producer);
    thread cons(consumer);

    prod.join();
    cons.join();

    return 0;
}

Output:

user@ubuntu:~/cpp/part_1/chapter_5$ ./cond_var 
producer - Pushed data to shared queue.
producer - Notifying consumer and sleeping.
consumer - Got 100 from shared queue.
consumer - Waiting for notify.
producer - Pushed data to shared queue.
producer - Notifying consumer and sleeping.
consumer - Got 101 from shared queue.
consumer - Waiting for notify.
producer - Pushed data to shared queue.
producer - Notifying consumer and sleeping.
consumer - Got 102 from shared queue.
consumer - Waiting for notify.
producer - Pushed data to shared queue.
producer - Notifying consumer and sleeping.
consumer - Got 103 from shared queue.
consumer - Waiting for notify.
producer - Pushed data to shared queue.
producer - Notifying consumer and sleeping.
consumer - Got 104 from shared queue.
consumer - Waiting for notify.
^C

Type functions are functions that is evaluated at compile-time give a type as its argument or returning a type. For example, computing the smallest supported positive float on a system. Also it could be used to find the byte width of standard types. This is an example of metaprogramming.

// g++ -std=c++11 -o smallfloat smallfloat.cpp
#include <iostream>
#include <limits>

using namespace std;

int main()
{   
    constexpr float min = numeric_limits<float>::min();
    cout << "smallest float on this system at compile time: " << min << endl;

    constexpr int szi = sizeof(int);
    cout << "int width in bytes on this system at compile time: " << szi << endl;

    return 0;
}

Output:

user@ubuntu:~/cpp/part_1/chapter_5$ ./smallfloat 
smallest float on this system at compile time: 1.17549e-38
int width in bytes on this system at compile time: 4

Random numbers are useful in many contexts, such as testing, games, simulation, and security. The diversity of application areas is reflected in the wide selection of random number generators provided by the standard library in <random>.” There are two parts to a random number generator: engine and the distribution. The engine outputs the values while the distribution maps the values into a range. Some included distributions: uniform_int_distribution, normal_distribution, and exponential_distribution. There are all kinds of possibilities with this <random> library.

// g++ -std=c++11 -o rando rando.cpp
#include <random>
#include <functional>
#include <chrono>
#include <iostream>

using namespace std;

int main()
{   
    default_random_engine engine {};
    // Seed the engine so it makes new numbers each time.
    engine.seed(std::chrono::system_clock::now().time_since_epoch().count());
    // Set the value map.
    uniform_int_distribution<> distro {1, 100};
    // Bind the engine and map together. Provided by <functional>
    // bind() makes a function object that will invoke its first argument
    // given its second argument as its argument. So, distro(engine);
    auto die = bind(distro, engine);
    for (int i = 0; i < 10; i++) {
        cout << die() << endl;
    }

    return 0;
}

It is necessary to seed the generator. Otherwise, it will output the same “random” numbers each run. Output:

user@ubuntu:~/cpp/part_1/chapter_5$ ./rando 
1
70
83
69
84
73
45
47
71
50
user@ubuntu:~/cpp/part_1/chapter_5$ ./rando 
67
99
64
61
93
62
94
82
29
49