Concurrency related bugs

  1. Unwanted blocking [Deadlock, Livelock, I/O]
  2. Race conditions

How to debug

  1. Run the smallest amount of code that triggers problem
  2. Eliminate the concurrency to check for non-concurrency issues
  3. Run on multicore vs single core.

How to test

  1. Test on multiple different threads with multiple different states
  2. Check if there are compiler/architecture specific behaviours
  3. Stress test with many threads running

Special implementation of synchronization primitives

Memory Debugging tools

Memory tools keep track of the state of the memory. Computes happens-before to find the data races. (2 accesses without happens-before accessing same memory location, one of them being a write).

Valgrind

Tool for memory debugging, memory leak detection, profiling

Memcheck: Memory shadow

For every byte of memory location:

  1. A bit (is the byte accessible)
  2. V bit (is byte initialized)

Detect and report incorrect memory access:

Valgrind Shadow Memory

Helgrind

Detect data race and race conditions

Sanitizers

Examples:

ASan: Address Sanitizer

TSan: Thread Sanitizer

Replaces your library calls with code that

Race conditions

Data races: Undefined behaviour due to concurrent memory access by at least two threads, at least one of which is a write, of the same memory location.

Broken invariants:

Lifetime issues:

Methods

Eliminate the concurrency

Run on a single core

How many threads can my program run for optimal performance?

Processor architectures: x86 runs on TSO, so rel-acq may work better than expected lol

Library calls, operators may NOT be thread safe!

Tutorial: Example with shared_ptr

Lock version:

template <typename T>
class shared_ptr {
private:
    std::mutex* mut;
    T* val;
    int* rc;
public:
    shared_ptr(T _val) : val(new T(_val)), rc(new int(1)), mut(new std::mutex) {}

    shared_ptr(const shared_ptr<T>& copy) :
        mut(copy.mut), val(copy.val), rc(copy.rc)
     {
        std::lock_guard lock(*mut);
        *rc += 1;
    }

    shared_ptr& operator=(const shared_ptr<T>& copy) {
        std::lock_guard lock(*copy.mut);
        mut = copy.mut;
        val = copy.val;
        rc = copy.rc;
        *rc += 1;
        return this;
    }

    ~shared_ptr() {
        std::lock_guard lock(*mut);
        *rc -= 1;
        if (*rc == 0) {
            delete val;
            delete rc;
            delete mut;
            std::cerr << "terminated!" << std::endl;
        }
    }

    T* get() { return val; }

    void set(T _val) {
        std::lock_guard lock(*mut);
        *val = _val;
    }

    const T* get() const { return val; }

    /*
    int use_count() {
        return rc->load();
    }
    */
};

Atomic rc version:

template <typename T>
class shared_ptr {
private:
    T* val;
    std::atomic<int>* rc = nullptr;
public:
    shared_ptr(T _val) : rc(new std::atomic<int>), val (new T(_val)) {}

    shared_ptr(const shared_ptr<T>& copy) :
        val(copy.val), rc(copy.rc) {
        rc->fetch_add(1);
    }

    shared_ptr& operator=(const shared_ptr<T>& copy) {
        val = copy.val;
        rc = copy.rc;
        rc->fetch_add(1);
        return this;
    }

    ~shared_ptr() {
        if (rc->fetch_sub(1) == 0) {
            delete val;
            delete rc;
            std::cerr << "terminated!" << std::endl;
        }
    }

    T* get() { return val; }

    const T* get() const { return val; }
};