7 min read

Categories

Tags

This post is going to explore Concurrency using C++. These posts Process synchronization in Operating Systemsand Process synchronization in Linux Kernel explains the theory in detail.


Table of Contents

  1. Thread Basics
    1. Thread API’s in C++
    2. Create a thread
    3. Thread ID and Cores
  2. Locks
    1. Without Lock
    2. With Mutex
    3. Types of Mutexes
      1. lock_guard vs unique_lock



Thread Basics

Thread API’s in C++

Function Description
t.join() Waits until thread t has finished its executable unit.
t.detach() Executes the created thread t independently of the creator.
t.joinable() Returns true if thread t is still joinable.
t.get_id() Returns the identity of the thread.
thread::hardware_concurrency() Returns the number of cores, or 0 if the runtime can not determine the number. Indicates the number of threads that can be run concurrently.
this_thread::sleep_until(absTime) Puts thread t to sleep until the time point absTime. Needs a time point or a time duration as an argument.
this_thread::sleep_for(relTime) Puts thread t to sleep for the time duration relTime. Needs a time point or a time duration as an argument.
this_thread::yield() Enables the system to run another thread.
t.swap(t2) Swaps the threads.
swap(t1, t2) Swaps the threads.



Create a thread

Thread can be created by calling std::thread t1(). Thread needs a starting point of the execution which is particularly a function.

// Thread creation with non-class function
std::thread t2(nonClassFunction, "Inside non-class function");

// Thread creation with class function
std::thread t1(&printing::classFunctionPrint, obj);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <thread>

using namespace std;

class printing {
private:
    string name;
public:
    printing() {}
    printing(string n):name(n) {}

    void classFunctionPrint() {
        cout << name << endl;
    }
};

void nonClassFunction(string n)
{
    cout << n << endl;
}


int main(int argc, const char * argv[]) {

    // Thread create for class function
    printing *obj = new printing("Inside class function");

    std::thread t1(&printing::classFunctionPrint, obj);

    t1.join();
    delete obj;

    // Thread create for non-class function
    std::thread t2(nonClassFunction, "Inside non-class function");
    t2.join();

    return 0;
}



Thread ID and Cores

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <thread>

using namespace std;

class printing {
private:
    string name;
public:
    printing() {}
    printing(string n):name(n) {}

    void classFunctionPrint() {
        cout << name << endl;
    }
};

void nonClassFunction(string n)
{
    cout << n << endl;
}


int main(int argc, const char * argv[]) {

    cout << "Total number of Cores = "<< thread::hardware_concurrency() << endl;

    // Thread create for class function
    printing *p = new printing("Inside class function");

    std::thread t1(&printing::classFunctionPrint, p);
    cout << "FROM MAIN: id of t1 " << t1.get_id() << endl;

    // Thread create for non-class function
    std::thread t2(nonClassFunction, "Inside non-class function");
    cout << "FROM MAIN: id of t2 " << t2.get_id() << endl;

    t1.join();
    delete p;
    t2.join();

    return 0;
}
Output:
Total number of Cores = 4
FROM MAIN: id of t1 0x700008ac1000
FROM MAIN: id of t2 0x700008b44000
Inside class function
Inside non-class function



Locks

Without Lock

This code creates two threads and they both try to increment the counter without synchronization primitives.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <thread>

using namespace std;

class printing {
private:
    string name;
    int count;
public:
    printing() {}
    printing(string n, int c):name(n), count(c) {}

    void classFunctionPrint() {
        cout << name << endl;
    }

    void incrementAndPrint(string threadName) {
        cout << "thread: " << threadName << " incrementing count from " << count << " to " << count + 1 << endl;
        count++;
        cout << "thread: " << threadName << " incremented count from " << count - 1 << " to " << count << endl;
    }
};

void nonClassFunction(string n)
{
    cout << n << endl;
}


int main(int argc, const char * argv[]) {

    cout << "Total number of Cores = "<< thread::hardware_concurrency() << endl;

    // Thread create for class function
    printing *p = new printing("Inside class function", 0);


    std::thread t3(&printing::incrementAndPrint, p,"thread3");
    std::thread t4(&printing::incrementAndPrint, p,"thread4");


    t3.join();
    t4.join();
    delete p;


    return 0;
}
Total number of Cores = 4
thread: thread3 incrementing count from 0 to 1
thread: thread: thread4thread3 incremented count from  incrementing count from 0 to 1 to 2
thread: thread41
 incremented count from 1 to 2

As we can see the output is all messed up.


With Mutex

#include <mutex>

std::mutex coutMutex;
coutMutex.lock();
coutMutex.unlock();


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

std::mutex coutMutex;

class printing {
private:
    string name;
    int count;
public:
    printing() {}
    printing(string n, int c):name(n), count(c) {}

    void classFunctionPrint() {
        cout << name << endl;
    }

    void incrementAndPrint(string threadName) {
        coutMutex.lock();
        cout << "thread: " << threadName << " incrementing count from " << count << " to " << count + 1 << endl;
        count++;
        cout << "thread: " << threadName << " incremented count from " << count - 1 << " to " << count << endl;
        coutMutex.unlock();
    }
};

void nonClassFunction(string n)
{
    cout << n << endl;
}


int main(int argc, const char * argv[]) {

    cout << "Total number of Cores = "<< thread::hardware_concurrency() << endl;

    // Thread create for class function
    printing *p = new printing("Inside class function", 0);


    std::thread t3(&printing::incrementAndPrint, p,"thread3");
    std::thread t4(&printing::incrementAndPrint, p,"thread4");


    t3.join();
    t4.join();
    delete p;


    return 0;
}
Output:
Total number of Cores = 4
thread: thread3 incrementing count from 0 to 1
thread: thread3 incremented count from 0 to 1
thread: thread4 incrementing count from 1 to 2
thread: thread4 incremented count from 1 to 2



Types of Mutexes

Classes Member Functions Constants
Mutexes    
mutex lock
unlock
try_lock
 
recursive_mutex lock
unlock
try_lock
 
timed_mutex lock
unlock
try_lock
try_lock_for
try_lock_until
 
recursive_timed_mutex lock
unlock
try_lock
try_lock_for
try_lock_until
 
Locks    
lock_guard   adopt_lock_t
defer_lock_t
unique_lock   adopt_lock_t
defer_lock_t

We have seen basic mutex above and other kind of mutexes are just the version of basic mutex with time properties.

Mutex help us get atomically inside critical section to perform operation but situation can be that in critical section thread t1 tries to acquire another lock L2 which is busy currently. In this case current thread t1 has to wait for lock L2 but holding lock L1. A deadlock can occur if other thread t2 is in same situation waiting for release of lock L1 from the thread t1 but holding lock L1.

Similar deadlock can also occur if thread t1 forgets to release lock possibly due to bug. In this situation, lock_guard comes to the rescue. lock_guard limits the life of lock until the scope of curly braces and then the lock is relesed.

{
  std::mutex m,
  std::lock_guard<std::mutex> lockGuard(m);
  sharedVariable= getVar();
}

mutex m will be released after curly braces even though unlock() is not called.


lock_guard vs unique_lock

At this point, we have two issues i.e.

  • Lock entity itself (mutex) i.e. lock object and various functions which can act on lock object i.e. lock(), unlock()
  • Management of locks i.e. acquiring and reliabily releasing locks, lifecycle of locks

As pointed above, deadlocks are primarily result of management of locks. To solve deadlock problem the approach is acquire all the locks together and if all the lock can not be acquired together then wait until it can be, until then no locks are acquired by the thread.

Once the locks are acquired the next step is ensure the lifecycle of locks are healty i.e. by end of our work locks are released.

For lifecycle and management we can use lock_guard or unique_lock.

  • lock_guard - It is locked only once on construction and unlocked on destruction.
  • unique_lock - You can lock and unlock a std::unique_lock any number of times in the method. This class guarantees an unlocked status on destruction.

We have two constants which works with above locking management

  • adopt_lock - Assumes that mutex object is already locked by the current thread
  • defer_lock - Makes it not to lock the mutex object automatically on construction
  • If none are provided then mutex is locked by lock_guard or unique_lock
// Case 1
std::mutex m;
std::lock_guard<std::mutex> lockGuard(m);  <== Acquires lock

// Case 2
std::unique_lock<std::mutex> lockGuard(m); <== Acquires lock


// Case 3
{
    std::mutex m1;
    std::mutex m2;

    unique_lock<mutex> guard1(m1, defer_lock);
    unique_lock<mutex> guard2(m2, defer_lock);

    lock(guard1,guard2);
} <== Locks are released at the end of scope

// Case 4
{
    std::mutex m1;
    std::mutex m2;

    lock(guard1,guard2);
    unique_lock<mutex> guard1(m1, adopt_lock);
    unique_lock<mutex> guard2(m2, adopt_lock);
} <== Locks are released at the end of scope

// Case 5
{
    std::mutex m1;
    std::mutex m2;

    lock(guard1,guard2);
    std::lock_guard<std::mutex> guard1(m1, adopt_lock);
    std::lock_guard<std::mutex> guard2(m2, adopt_lock);
    ...
} <== Locks are released at the end of scope

// Case 6
{
    std::mutex m1;
    std::mutex m2;

    std::lock_guard<std::mutex> guard1(m1, defer_lock);
    std::lock_guard<std::mutex> guard2(m2, defer_lock);

    lock(guard1,guard2);
    ...
} <== Locks are released at the end of scope