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


