Skip to main content
Parallel Programming advanced Lesson 3 of 3

Lock-Free Parallel Algorithm Patterns

Explore advanced parallel patterns: lock-free counters, parallel prefix/reduction trees, and safe contention management.

Advanced Pattern Mindset (Theory)

At the advanced level, concurrency bugs become:

  • timing-dependent (rare races)
  • ordering-dependent (memory model issues)
  • performance-dependent (contention changes behavior)

Lock-free designs aim to:

  • eliminate blocking
  • minimize contention hotspots
  • use atomic operations correctly

Code Example 1 — Lock-Free-ish Counter with C++ atomics

Atomics provide safe single-variable updates without locks.

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

int main() {
  const int N = 1'000'00;
  const int T = 8;

  std::vector<int> a(N, 1);
  std::atomic<long long> sum{0};

  auto worker = [&](int tid) {
    int start = tid * (N / T);
    int end   = (tid + 1) * (N / T);

    long long local = 0;
    for (int i = start; i < end; i++) local += a[i];

    // Single shared update using atomic add (contention depends on T)
    sum.fetch_add(local, std::memory_order_relaxed);
  };

  std::vector<std::thread> threads;
  for (int tid = 0; tid < T; tid++) threads.emplace_back(worker, tid);
  for (auto& th : threads) th.join();

  std::cout << "sum=" << sum.load() << "\n";
}

When this helps

  • You reduce work locally, then do one atomic update per thread
  • That minimizes contention compared to atomics per element

Code Example 2 — Parallel Reduction Tree Skeleton

A reduction tree combines partial sums in stages. Even when implemented with locks/atomics, the tree structure reduces contention by aggregating locally first.

#include <vector>
#include <thread>
#include <iostream>

long long parallel_sum_tree(const std::vector<int>& a, int threads) {
  int N = (int)a.size();
  std::vector<long long> partial(threads, 0);

  auto worker = [&](int tid) {
    int start = tid * (N / threads);
    int end   = (tid + 1) * (N / threads);

    long long local = 0;
    for (int i = start; i < end; i++) local += a[i];
    partial[tid] = local;
  };

  std::vector<std::thread> ts;
  for (int t = 0; t < threads; t++) ts.emplace_back(worker, t);
  for (auto& th : ts) th.join();

  // Reduction tree over partial results (serial here for illustration)
  while ((int)partial.size() > 1) {
    int half = (int)partial.size() / 2;
    for (int i = 0; i < half; i++) partial[i] += partial[partial.size() - 1 - i];
    partial.resize(half + (partial.size() % 2));
  }
  return partial[0];
}

int main() {
  std::vector<int> a(1'000'000, 1);
  std::cout << "sum=" << parallel_sum_tree(a, 8) << "\n";
}

Why the tree matters

  • Instead of “many threads hammering one variable,” you aggregate in levels
  • In real systems, each level can be parallelized (e.g., with tasks)

Common Gotchas (Advanced)

  • Wrong memory ordering: using atomics incorrectly can produce subtle bugs.
  • Contention hotspots move: removing a lock can create a new hotspot at an atomic variable.
  • Non-associative floating point: results may differ slightly across scheduling/order.
  • ABA problem (advanced): lock-free pointer algorithms can be vulnerable without additional tagging/hazard pointers.

Quick Checklist

  • Reduce locally; update shared state infrequently
  • Prefer reduction trees over a single accumulator
  • Use atomics for single-variable correctness, not for complex state
  • Benchmark under realistic contention patterns

Frequently Asked Questions

What is lock-free?
Lock-free means at least one thread makes forward progress without waiting for a lock. It typically relies on atomic operations and careful memory ordering.
Is lock-free always faster?
No. Under low contention, locks can be simpler and fast. Lock-free shines when you need high concurrency and can avoid stalls—always benchmark.
What is a parallel prefix sum?
A prefix sum computes partial aggregates: output[i] = sum of inputs[0..i]. Parallel algorithms compute it in stages to expose concurrency.