Threading using C++11: Part 1

Recently I bumped into a great introductory video series on concurrent programming using C++11 by Bo Qian on youtube. In this I am just trying to note down important parts and playing with stuffs. Some of the examples may be directly used as it is in the video series.

Basic threading environment in C++11

The following example introduces with the basic constructs.

/**
 * filename: test1.cpp
 * a simple example of using threads in c++11
 * to compile, use
 *   g++ -lpthread -std=c++0x test1.cpp
 * on linux environment
 */

#include <iostream>
#include <thread>
using namespace std;

// a stupid thread function
void thread_function(int n)
{
  for (int i=0; i<n; ++i)
  // each thread has a unique id
    cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;
  cout << "address of n in t1 thread is " << &n << endl;
}

// a stupid thread function that takes a reference
void thread_function_ref(int& n)
{
  for (int i=0; i<n; ++i)
    cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;
  cout << "address of n in t2 (reference) thread is " << &n << endl;
}

// a stupid functor
class functor
{
  int state;
public:
  functor():state(0){};
  void operator()(int n) {
  for (int i=0; i<n; ++i)
    cout << "thread with id " << this_thread::get_id() << " says hello "<< i << endl;
  }
};

// a stupid main function
int main()
{
  int n = 3;

// thread creation -
// ----------------
  // thread using normal function - takes a callable object as the first
  // argument and a number of other arguments which are to be passed as
  // arguments to the callable object (function, lambda expressions, functor)
  // note that the arguments are always passed by value! a way to pass the
  // arguments by reference is shown in the next example
  thread t1(thread_function, n);

  // using std::ref we can pass the argument (n in this case) as a reference
  // just passing n won't do the trick!
  thread t2(thread_function, std::ref(n));
  // to verify, we print the address of n in the main thread and in the child
  // threads of the thread functions. let's see!
  cout << "address of n in main thread is " << &n << endl;

  // this is cool! using this we can share memory between threads. we can
  // also completely hand over a memory to another thread using std::move
  // but more on that later

// joining and stuffs -
// --------------------
  // we may decide to join the thread, in which the creator thread will wait
  // until the execution of the child thread is finished. here, however we
  // decide not to do that right away
  //
  //t1.join();

// more funky ways of thread creation -
// -----------------------------------
  // thread using lambda function - lambda functions are cool! check more
  // about these on cppreference.com
  thread t3([n]() {
    // inherits variable n from current scope and copies it to the thread
    // stack at different address
    for (int i=0; i<n; ++i)
      cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;
    cout << "address of n in t3 thread is " << &n << endl;
  });

  thread t4([&n]() {
    // inherits variable n from current scope and uses the same address
    for (int i=0; i<n; ++i)
      cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;
    cout << "address of n in t4 thread is " << &n << endl;
  });

  thread t5([](int n) {
    // doesn't inherit anything - we pass it by value - same as t1
    for (int i=0; i<n; ++i)
      cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;
    cout << "address of n in t5 thread is " << &n << endl;
  }, n);

  // similarly we can pass it by reference as t2 - but its getting boring

// thread using functor -
// ----------------------
  // creating an object first
  functor f1;
  thread t6(f1, n);

  // using anonymous object (note the extra paranthesis)
  thread t7((functor()), n);

// some more boring stuffs -
// -------------------------
  // something to do for the main thread
  for (int i=0; i<n; ++i)
    cout << "thread with id " << this_thread::get_id() << " says hello " << i << endl;

  // join/detatch all threads
  t1.detach();
  if (t1.joinable())
     t1.join();
  t2.join();
  t3.join();
  t4.join();
  t5.join();
  t6.join();
  t7.join();

  return 0;
}

This code works. When I compiled and ran on my system (Fedora-19-x86_64) it ran, except it printed out something that’s pretty tough to read!

[lambday@lambday.iitb.ac.in thread_test]$ ./a.out
address of n in main thread is thread with id thread with id 0x7fff867e5aec0x7f30625da7000x7f3061dd9700 says hello  says hello
0
thread with id 0139845777069824 says hello
thread with id 139845768677120 says hello 1
thread with id 139845777069824 says hello 2
address of n in t1 thread is thread with id 0x7f3061dd8e0c0x7f3060dd7700
thread with id  says hello 0
thread with id 139845760284416 says hello 1
thread with id 139845760284416 says hello 2
thread with id 139845751891712address of n in t4 thread is  says hello 0x7fff867e5aec0

thread with id 139845751891712 says hello 1
thread with id 139845751891712 says hello 2
address of n in t5 thread is 0x7f30605d5e04
0
thread with id 139845768677120 says hello 1
thread with id 139845768677120 says hello 2
address of n in t3 thread is 0x2138340
139845785462528 says hello 1
thread with id thread with id 139845785462528 says hello 2
address of n in t1 thread is 0x7f30625d9e0c
139845743499008 says hello 0
thread with id 139845743499008 says hello 1
thread with id 139845743499008 says hello 2
thread with id 139845785470848 says hello 0
thread with id 139845785470848 says hello 1
thread with id 139845785470848 says hello 2
thread with id 139845735106304 says hello 0
thread with id 139845735106304 says hello 1
thread with id 139845735106304 says hello 2

This is because all the threads are running for the same resource stdout and causing data race condition. This must be handled using mutex which provides lock/unlock mechanisms for shared resources. The following example shows how.

Handling data race condition using mutex

/**
 * filename: test2.cpp
 * a simple example of using a shared resource, such as cout,
 * among multiple threads securely using mutex
 */

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

class printer
{
  ostream& os;
  mutex& mu;
public:
  // making default constructor, copy constructor and
  // assignment operator implicitly disabled for apparently 
  // no reason
  printer()=delete;
  printer(const printer&)=delete;
  const printer& operator=(const printer&)=delete;

  // the constructor that we will be using
  printer(mutex& m):os(cout), mu(m){}

  // the function that we will be using
  // I tried using string& s as a parameter but didn't work!
  void shared_print(string s, int i)
  {
    // before using the resource that is os, we first need to
    // lock the mutex, which could have normally be done using
    // mutex::lock and mutex::unlock methods. however, if in
    // between lock and unlock some exception happens, then
    // the mutex is never going to be unlocked again until the
    // main thread dies, therefore all other threads will be
    // kept on waiting till death
    //
    // lock_guard provides a Resource Acquisition Is Initialization
    // (RAII) sort of thing, which calls the unlock in its destructor
    // therefore, whenever the control goes outside of this block and
    // guard gets killed, the mutex will be unlocked!
    // sweet!
    lock_guard<mutex> guard(mu);
    os << s << i << endl;
  }
};

void thread_function(int& n, printer& pr)
{
  for (int i=0; i<n; ++i)
    // tried using std::ref which gave weird errors
    // that it has been deleted!! any clue??
    pr.shared_print(string("t1():"),i);
}

int main()
{
  // mutex is the main tool for employing mutual exclusion from
  // shared resources
  mutex mu;

  printer pr(mu);
  int n = 4;

  // main thread and child thread use same printer object
  // to print stuffs. in fact everyone wishes to print stuffs
  // using cout should use this! otherwise cout will be exposed
  // to vulnerabilities and somebody can do nasty stuffs.
  //
  // its really importantn that we never ever give the access
  // of the ostream from printer to anybody!
  thread t1(thread_function, std::ref(n), std::ref(pr));

  // its a good practice to surround the following within
  // a try-catch block, so that if the following part of the
  // code throws an exception, the child thread still gets joined
  //
  try {
    for (int i=0; i<n; ++i)
      pr.shared_print(string("main():"),i);
  } catch(...) {
    t1.join();
    throw;
  }

  t1.join();

  return 0;
}	

This gives a pretty output

[lambday@lambday.iitb.ac.in thread_test]$ ./a.out
main():0
t1():0
t1():1
t1():2
t1():3
main():1
main():2
main():3

Number of threads to be created is often a good question. It makes sense to create as many threads as there are cpus available. Otherwise a lot of time would be wasted in context switching which is a bad thing for program performance.

std::thread::hardware_concurrency()

function provides a way to do that.

In the next example, a different kind of lock guard is shown at action, which provides a deferred locking mechanism.

/**
 * filename: test3.cpp
 * this shows usage of unique_lock
 */

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

// similar to what we have seen before, locks the data
// and then writes to it
void thread_function(int& data, mutex& mu, void (*f)(int&))
{
  // we shouldn't create a lock_guard here because there is
  // no point in locking the data between the iterations of
  // the loop - the other thread would then have to wait until
  // this function returns from one thread
  unique_lock<mutex> guard(mu, defer_lock);
  for (int i=0; i<10; ++i)
  {
    // here we can create the lock as lock_guard and before the
    // next iteration the mutex will be unlocked. but to avoid
    // the overhead of creating and destroying a lock_guard obj
    // each time, we can use unique lock's lock/unlock mechanism
    // under defered lock
  
    //lock_guard<mutex> guard(mu);
    //
    guard.lock();
    f(data);
    cout << this_thread::get_id() << ": says data is " << data << endl;
    guard.unlock();
  }
}

int main()
{
  mutex mu;
  // data that the threads try to write into
  // must be synchronized to avoid data race condition
  // using mutex
  int data = 0;

  // creating a thread that tries to increase the data
  thread t1(thread_function, std::ref(data), std::ref(mu), [](int& n){n++;});
  // another thread which tries to decrease the data
  thread t2(thread_function, std::ref(data), std::ref(mu), [](int& n){n--;});

  t1.join();
  t2.join();

  return 0;
}

Output is shown below. after the execution is over, the value of data is same as before, as one might expect.

[lambday@lambday.iitb.ac.in thread_test]$ ./a.out
140657387300608: says data is 1
140657387300608: says data is 2
140657387300608: says data is 3
140657387300608: says data is 4
140657387300608: says data is 5
140657387300608: says data is 6
140657378907904: says data is 5
140657378907904: says data is 4
140657378907904: says data is 3
140657378907904: says data is 2
140657378907904: says data is 1
140657378907904: says data is 0
140657378907904: says data is -1
140657378907904: says data is -2
140657378907904: says data is -3
140657378907904: says data is -4
140657387300608: says data is -3
140657387300608: says data is -2
140657387300608: says data is -1
140657387300608: says data is 0

Some more pointers are coming up next! Wrapping up this one cause its getting too big.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s