liveBook · Manning

Resource contention

As explained in C++ Concurrency in Action, Second Edition

Thread contention is competition among threads for access to shared data, a resource, or a synchronization primitive.

Resource contention occurs when threads compete to access shared data or synchronization primitives. High contention makes processors wait for one another; low contention means they rarely wait. Transfers of shared data between processor caches can add delay even when no thread is explicitly waiting. section 8.2.2

std::atomic<unsigned long> counter(0);
void processing_loop()
{
    while(counter.fetch_add(1,std::memory_order_relaxed)<100000000)
    {
        do_something();
    }
}

A shared atomic counter in a loop can pass between processor caches repeatedly, causing cache ping-pong and reducing performance. section 8.2.2

std::mutex m;
my_data data;
void processing_loop_with_mutex()
{
    while(true)
    {
        std::lock_guard<std::mutex> lk(m);
        if(done_processing(data)) break;
    }
}

Repeatedly acquiring a mutex can also contend: threads transfer and modify the mutex data, in addition to waiting for the lock. section 8.2.2

Design choices can reduce contention, but may introduce other performance risks:

  • Change data layout or assign data elements differently among threads; consider contention alongside false sharing and data proximity. section 8.3
  • Per-thread work queues can reduce competition for a shared queue, but uneven work distribution may leave one thread overloaded while others have little to do. section 9.1.4
  • A shared stack of pending work can remain highly contended even when it is lock-free. section 8.1.2
Listing 9.6. A thread pool with thread-local work queues
class thread_pool
{
    threadsafe_queue<function_wrapper> pool_work_queue;
    typedef std::queue<function_wrapper> local_queue_type;   #1
    static thread_local std::unique_ptr<local_queue_type>
        local_work_queue;                                    #2
    void worker_thread()
    {
        local_work_queue.reset(new local_queue_type);        #3

        while(!done)
        {
            run_pending_task();
        }
    }
public:
    template<typename FunctionType>
    std::future<typename std::result_of<FunctionType()>::type>
        submit(FunctionType f)
    {
        typedef typename std::result_of<FunctionType()>::type result_type;
        std::packaged_task<result_type()> task(f);
        std::future<result_type> res(task.get_future());
        if(local_work_queue)                                 #4
        {
            local_work_queue->push(std::move(task));
        }
        else
        {
            pool_work_queue.push(std::move(task));           #5
        }
        return res;
    }
    void run_pending_task()
    {
        function_wrapper task;
        if(local_work_queue && !local_work_queue->empty())  #6
        {
            task=std::move(local_work_queue->front());
            local_work_queue->pop();
            task();
        }
        else if(pool_work_queue.try_pop(task))              #7
        {
            task();
        }
        else
        {
            std::this_thread::yield();
        }
    }
    // rest as before
};


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":3,\"ch\":61},{\"line\":3,\"ch\":63}],[{\"line\":5,\"ch\":61},{\"line\":5,\"ch\":63}],[{\"line\":8,\"ch\":61},{\"line\":8,\"ch\":63}],[{\"line\":23,\"ch\":61},{\"line\":23,\"ch\":63}],[{\"line\":29,\"ch\":61},{\"line\":29,\"ch\":63}],[{\"line\":36,\"ch\":60},{\"line\":36,\"ch\":62}],[{\"line\":42,\"ch\":60},{\"line\":42,\"ch\":62}]]"}
!@%STYLE%@!

This thread-pool implementation illustrates using thread-local work queues alongside a shared pool queue. section 9.1.4

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Explains waiting, cache transfers, and cache ping-pong, then discusses layout and queue designs that reduce contention.
Examples
Uses a shared atomic counter, a mutex loop, per-thread work queues, and a shared stack of pending work.
What you can do after
Diagnose performance risks by identifying shared counters, mutex loops, queues, and stacks that can become contended.
How it connects
Connects contention with cache ping-pong, mutexes, atomic operations, false sharing, and thread pools.

Also explained in

Compare how 3 books explain resource contention →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.