Introduction to Parallel Processing
Complete revision notes. Everything in this unit, organised by topic — definitions, diagrams, formulas, comparison tables and worked examples. Revise from this alone and you have the whole unit.
Contents
A · Why parallelism
B · Taxonomy & memory
Quick recall — every table and formula in one place · Self-check questions
Part A — Why Parallelism?
A1. The free lunch is over
For roughly forty years, programs got faster every year without programmers doing anything. Clock speeds rose, and the same binary simply ran quicker on next year's processor. That era ended around 2005.
It ended because of the power wall. The dynamic power a chip consumes is approximately
P = C x V^2 x f
C = capacitance, V = voltage, f = clock frequency
Raising the clock frequency requires raising the voltage to keep the transistors switching reliably, and because power depends on the square of voltage, the power consumed rises far faster than the performance gained. Past roughly 4 GHz the heat generated could no longer be removed economically. The related memory wall (Unit II) and instruction-level parallelism wall compounded the problem.
Manufacturers responded by keeping clock speeds roughly constant and putting more cores on each chip instead. Performance is still increasing — but only for programs written to use many cores. Serial programs stopped getting faster. Parallelism moved from an optimisation to a requirement.
This is the single fact that motivates the entire subject: hardware is now parallel by default, so software must be parallel to use it.
A2. Concurrency vs parallelism vs multitasking
Three terms that are constantly confused. The distinction is examinable and worth stating precisely.
| Term | Meaning | Concerns | Needs many cores? |
|---|---|---|---|
| Concurrency | Dealing with many things at once | Program structure — how work is organised into independently progressing tasks | No |
| Parallelism | Doing many things at once | Execution — genuinely simultaneous work | Yes |
| Multitasking | The OS switching between tasks on shared cores | Scheduling — the illusion of simultaneity | No |
THE CHEF ANALOGY (remember this — it answers the question by itself)
1 chef, 1 dish -> sequential. No concurrency, no parallelism.
1 chef, 3 dishes, juggling -> CONCURRENCY. One core, progress on many
tasks by switching cleverly.
3 chefs, 3 dishes, at once -> PARALLELISM. Real simultaneous work.
Needs 3 chefs = 3 cores.
The one-line summary: concurrency is about structure; parallelism is about execution. A concurrent program can run on a single core; a parallel program cannot achieve parallelism without multiple processing units. Concurrency makes parallelism possible, but does not guarantee it.
A3. Latency vs throughput
Latency — how long a single task takes, from start to finish.
Throughput — how many tasks complete per unit of time.
These are independent quantities and improving one need not improve the other. Opening more toll booths does not make any individual car pass through faster, but far more cars get through per minute.
Worked example — the canteen counter. One counter serves a student in 2 minutes; 120 students are waiting.
| Counters | Latency (per student) | Throughput (students/hr) | Queue cleared in |
|---|---|---|---|
| 1 | 2 min | 30 | 240 min |
| 2 | 2 min — unchanged | 60 | 120 min |
| 4 | 2 min — unchanged | 120 | 60 min |
| 8 | 2 min — unchanged | 240 | 30 min |
The latency column never changes. No individual student is served faster, yet the queue clears eight times sooner. The same holds in a real system: an API taking 200 ms per request and handling 5 requests/sec will, behind a load balancer with four servers, handle 20 requests/sec — while every single request still takes 200 ms.
Most parallelism is a throughput win, not a latency win. A GPU does not compute one pixel faster; it computes millions at once. This is why adding servers fixes a site that is slow under load, but never fixes a page that is simply slow.
A4. Data parallelism vs task parallelism
| Data parallelism | Task parallelism | |
|---|---|---|
| Definition | The same operation applied to many data items simultaneously | Different operations running simultaneously |
| What is split | The data | The work / the functions |
| Example | Add 1 to every element of a million-item array; split the array, each core takes a chunk | One thread loads a file, another compresses it, another uploads it |
| Analogy | 100 students each grade one page of the same exam | One person chops, another fries, another plates |
| Scales with | Data size — usually very well | Number of distinct tasks — usually limited |
| Hardware fit | GPUs, SIMD units, #pragma omp parallel for | Multicore CPUs, threads, OpenMP sections |
Most real systems mix both. Data parallelism generally offers far more scope for scaling, because the amount of data typically grows without bound while the number of distinct tasks in a program does not.
A5. Speedup, and the ceiling
time on 1 core T(1)
Speedup S = ---------------- = ------
time on N cores T(N)
Perfect (linear) speedup = N on N cores. Rarely achieved.
100 s on 1 core -> 25 s on 4 cores = 4x (perfect)
100 s on 1 core -> 40 s on 4 cores = 2.5x (real life)
Speedup falls short of linear because some work is inherently sequential — step 2 requires the result of step 1 — and because parallelism introduces coordination overhead. Reading input, distributing work and combining results generally cannot be split.
Worked example — the ceiling is real. A job takes 10 hours, of which 1 hour is setup that cannot be split. The remaining 9 hours split perfectly.
| Cores | Sequential part | Parallel part | Total | Speedup |
|---|---|---|---|---|
| 1 | 1 hr | 9 hrs | 10 hrs | 1× |
| 2 | 1 hr | 4.5 hrs | 5.5 hrs | 1.8× |
| 10 | 1 hr | 0.9 hr | 1.9 hrs | 5.3× |
| 100 | 1 hr | 0.09 hr | 1.09 hrs | 9.2× |
| 1,000,000 | 1 hr | ≈ 0 | ≈ 1 hr | 9.99× |
| ∞ | 1 hr | 0 | 1 hr | 10× — never more |
Going from 100 cores to a million takes the speedup from 9.2× to 9.99×. Adding 999,900 cores gains almost nothing. That single un-splittable hour caps the program at 10× forever. The sequential fraction, not the core count, decides the outcome. This is Amdahl's Law, formalised in Unit III. As a preview: if a fraction f of the work is parallelisable, maximum speedup is 1/(1 − f).
A6. The new classes of bug
Parallelism introduces failure modes that simply do not exist in sequential code.
| Bug | What happens | Why it is hard |
|---|---|---|
| Race condition | Two threads update the same variable concurrently; the result depends on timing | Non-deterministic — may pass a thousand tests and fail in production |
| Deadlock | Two threads each hold a resource the other needs, and both wait forever | The program hangs with no error and no output |
| Livelock | Threads keep changing state in response to each other but make no progress | Looks busy; achieves nothing |
| Load imbalance | One thread receives most of the work while others sit idle | Silent — the program is correct, just slow |
| Starvation | A thread never gets scheduled or never acquires the lock it needs | Intermittent and load-dependent |
Worked example — a race condition, step by step. A bank balance is ₹1000. Two people withdraw ₹600 each from two ATMs at the same instant.
| Time | ATM A | ATM B | Balance in DB |
|---|---|---|---|
| t1 | reads balance → 1000 | 1000 | |
| t2 | reads balance → 1000 | 1000 | |
| t3 | 1000 − 600 = 400 ✓ | 1000 | |
| t4 | 1000 − 600 = 400 ✓ | 1000 | |
| t5 | writes 400 | 400 | |
| t6 | writes 400 | 400 |
₹1200 was dispensed and the balance reads ₹400. The bank lost ₹600. Every individual step was arithmetically correct — the bug lives entirely in the interleaving. Change the timing by a microsecond and it works. This is exactly the same bug, in the same shape, as two threads executing sum += a[i] (see C4).
Part B — Flynn's Taxonomy & Memory Models
B1. Flynn's taxonomy
Proposed by Michael Flynn in 1966, this is the standard classification of computer architectures. It classifies a machine along two axes: the number of concurrent instruction streams and the number of concurrent data streams. Two binary choices give four categories.
| SINGLE DATA | MULTIPLE DATA
-------------------+----------------+------------------
SINGLE INSTRUCTION | SISD | SIMD
-------------------+----------------+------------------
MULTIPLE INSTRUCTN | MISD | MIMD
Do not memorise — decode. SIMD = Single Instruction, Multiple Data.
Say the letters and the meaning follows.
| Class | Meaning | Analogy | Real examples |
|---|---|---|---|
| SISD | One instruction stream operating on one data stream, one item at a time. The classic sequential von Neumann machine. | One chef, one recipe, one dish | A single-core processor; early PCs |
| SIMD | One instruction stream applied simultaneously to many data items. The basis of data parallelism. | A drill sergeant's single command; 100 soldiers | GPUs; vector units (SSE, AVX, NEON); array processors |
| MISD | Many instruction streams operating on the same data stream. Rare in practice. | Several inspectors checking one product in different ways | Fault-tolerant systems with redundant voting, e.g. flight-control computers; systolic arrays are sometimes classified here |
| MIMD | Independent processors, each with its own instruction stream and its own data. The common case. | An office where everyone does a different task | Multicore CPUs; clusters; every modern server and laptop |
MIMD is by far the most common class in general-purpose computing; SIMD dominates wherever the same operation must be applied to large regular data, which is why GPUs are SIMD-like. MISD is largely of theoretical interest, though it is genuinely used in safety-critical redundant systems.
B2. SPMD — what real programs actually do
SPMD — Single Program, Multiple Data. Every processor runs the same program on different data, and each may follow a different path through that program by branching on its own rank or thread ID.
if (my_rank == 0):
read the file, hand out the chunks
else:
wait for my chunk, then process it
# ONE program. Rank 0 takes a different path from rank 7.
Nobody hand-writes a separate program for each processor, so SPMD is the pattern nearly all real parallel code follows — both MPI and OpenMP programs are SPMD. Note carefully: SPMD is not a fifth Flynn category. It is a practical special case of MIMD. Flynn classifies the hardware; SPMD describes how we write software for that hardware.
B3. Shared memory
The second way to classify a parallel system is by how processors share data. In a shared-memory architecture, all processors access one common pool of RAM. If core 3 writes x = 5, core 7 can simply read x and see it. Communication happens implicitly, through ordinary variables.
SHARED MEMORY
+----+ +----+ +----+ +----+
| P0 | | P1 | | P2 | | P3 |
+----+ +----+ +----+ +----+
| | | |
===+=======+=======+=======+=== interconnect
|
+---------------+
| SHARED RAM |
+---------------+
Analogy: roommates sharing one fridge. Taking food is easy because it is all right there, but you bump into each other, and two people reaching for the last egg simultaneously is a collision — a race condition.
Advantages: a simple mental model, no explicit data movement, and easy programming with threads or OpenMP. Disadvantages: it does not scale past tens of cores, it requires hardware cache coherence (Unit II), and it is vulnerable to race conditions and contention.
B4. UMA vs NUMA
Shared memory subdivides according to whether all memory is equally distant from all cores.
| UMA — Uniform Memory Access | NUMA — Non-Uniform Memory Access | |
|---|---|---|
| Access time | Identical from every core to every location | Local memory fast, remote memory slower |
| Structure | One memory pool behind a shared bus or crossbar | Memory banks attached to individual sockets, joined by an interconnect |
| Analogy | Every fridge is the same few steps away | Your fridge is here; your roommate's is across town |
| Typical system | Classic SMP; a laptop or single-socket desktop | Every multi-socket server |
| Scalability | Limited — the shared path saturates | Better, but performance depends on data placement |
Worked example — what NUMA actually costs. A two-socket server, 32 cores and 256 GB per socket. Local access ≈ 80 ns; remote access across the interconnect ≈ 140 ns, i.e. 1.75× slower.
| Data placement | Average latency | Total time (10⁸ accesses) | vs best |
|---|---|---|---|
| 100% local (pinned correctly) | 80 ns | 8.0 s | 1.00× |
| 50/50 (the OS guessed) | 110 ns | 11.0 s | 1.38× slower |
| 100% remote (worst case) | 140 ns | 14.0 s | 1.75× slower |
Same binary, same input, same core count — a 75% difference in runtime determined solely by where the data physically sits. Nothing in the source code reveals which row you are in. NUMA is the underlying reason that simply adding more CPUs to one machine eventually stops helping: it is the physics behind the ceiling on vertical scaling.
B5. Distributed memory
In a distributed-memory architecture each processor has its own private memory and no processor can directly address another's. The only way to share data is to send a message across a network. This is precisely the model MPI implements.
DISTRIBUTED MEMORY
+----+ +-----+ +----+ +-----+ +----+ +-----+
| P0 |-| RAM | | P1 |-| RAM | | P2 |-| RAM |
+----+ +-----+ +----+ +-----+ +----+ +-----+
| | |
+==================+==================+
NETWORK
(the ONLY path between nodes: messages)
Analogy: everyone has their own fridge in their own house. To share food you must phone and deliver. More coordination is required, but you can keep adding houses indefinitely.
| Shared memory | Distributed memory | |
|---|---|---|
| Communication | Implicit — via shared variables | Explicit — via messages |
| Programming difficulty | Easier | Harder |
| Scalability | Tens of cores | Thousands of nodes |
| Data movement | Handled by hardware | Written by the programmer |
| Needs cache coherence? | Yes | No |
| Typical tool | OpenMP, pthreads | MPI |
| Typical system | A single server | A cluster or supercomputer |
The trade-off in one line: shared memory is easy to program and hard to scale; distributed memory is hard to program and scales without limit.
B6. Hybrid architectures
Every modern supercomputer is hybrid: a cluster of nodes connected by a fast network (distributed memory), where each node is itself a shared-memory multicore machine, usually with one or more GPUs attached.
NODE 0 NODE 1
+---------------------+ +---------------------+
| core core core core | | core core core core | <- OpenMP
| shared RAM | | shared RAM | inside
| [GPU] | | [GPU] | <- CUDA
+---------------------+ +---------------------+
| |
+==============================+
NETWORK <- MPI between
Three tools appear in one program: MPI moves data between nodes, OpenMP uses the many cores within a node, and CUDA drives the GPU. The analogy is a neighbourhood of shared houses: inside each house roommates share one fridge, and between houses you phone and deliver.
Part C — Parallel Programming Models
C1. Three ways to split work
| Model | What is split | Description | Example |
|---|---|---|---|
| Data parallelism | The data | The same operation applied to different portions of the data, at the same time | Each core sums one quarter of an array |
| Task parallelism | The functions | Different, independent operations run at the same time | One thread parses, one validates, one writes |
| Pipeline parallelism | The stages | Work flows through a sequence of stages; each stage processes a different item simultaneously | An assembly line; a CPU instruction pipeline |
Why a pipeline feels faster. Suppose a job has 3 stages of 10 minutes each, and there are 6 jobs to process.
SEQUENTIAL: 6 jobs x 30 min = 180 min
PIPELINE: fill the pipe (2 stages) = 20 min
then one job completes every = 10 min
-> 20 + (6 x 10) = 80 min
Latency per job is STILL 30 minutes — no single job got faster.
Throughput rose from 1 job / 30 min to 1 job / 10 min.
Speedup approaches the number of stages, but only once the
pipeline is full — and only if the stages take equal time.
The slowest stage sets the rate for everybody.
This is the same latency-versus-throughput distinction from A3, and the same reason a pipeline with one slow stage performs badly — a form of load imbalance.
C2. The programming-models landscape
| Model | Memory architecture | Unit of parallelism | Communication | Scale |
|---|---|---|---|---|
| OpenMP | Shared | Thread | Shared variables | One node, tens of cores |
| MPI | Distributed | Process (rank) | Explicit messages | Thousands of nodes |
| CUDA / OpenCL | GPU device memory | GPU thread | Host–device copies | One GPU, thousands of threads |
| pthreads | Shared | Thread | Shared variables | One node; fully manual |
| MapReduce / Spark | Distributed cluster | Task on a data partition | Framework-managed shuffle | Hundreds of machines |
The model follows the memory architecture, not personal preference. Shared memory implies OpenMP or threads; distributed memory implies MPI; a GPU implies CUDA. Hybrid machines use several at once (B6).
C3. OpenMP and the fork-join model
OpenMP (Open Multi-Processing) is an API for shared-memory parallel programming in C, C++ and Fortran. It consists of compiler directives (#pragma omp ...), a runtime library and environment variables. Its defining characteristic is that parallelism is expressed by annotating existing serial code rather than rewriting it: compile without the -fopenmp flag and the pragmas are ignored, leaving a valid serial program.
OpenMP follows the fork-join execution model:
master thread
|
| #pragma omp parallel
FORK ------+-----+-----+-----+
| | | | | team of threads
| T0 T1 T2 T3 runs the region
| | | | |
JOIN ------+-----+-----+-----+ implicit barrier
|
| (serial again)
v
The program begins as a single master thread. At a parallel region it forks a team of threads that execute the region concurrently; at the end of the region the threads join at an implicit barrier and execution returns to serial. A program may fork and join many times.
Fork-join is not free. Creating and synchronising a team costs on the order of microseconds. Parallelising a loop that runs for less time than that makes the program slower. A small loop with #pragma omp parallel for can easily run several times slower than the serial version. Always parallelise the outer, long-running loop — never a short inner one.
The core toolkit.
#include <omp.h>
#pragma omp parallel // fork a team of threads
#pragma omp parallel for // fork + split loop iterations
#pragma omp critical // only one thread at a time here
#pragma omp barrier // all threads wait here
omp_get_thread_num() // my thread ID (0 .. n-1)
omp_get_num_threads() // size of the current team
omp_set_num_threads(4) // request a team size
compile with: gcc -fopenmp prog.c -o prog
Non-determinism. The order in which threads execute is not defined. A parallel loop printing thread IDs will print them in a different order on different runs, and both orders are correct. This is the first lesson of parallel programming: you do not own the order. Correctness must never depend on it.
C4. Race conditions and critical sections
Race condition — a situation in which the result of a computation depends on the relative timing of two or more threads accessing shared data, at least one of which is writing.
THE WRONG WAY
double sum = 0.0;
#pragma omp parallel for
for (i = 0; i < N; i++)
sum += a[i]; // <-- every thread writes the SAME variable
Run it 5 times on the same input and you get 5 different answers.
Why. sum += a[i] is not one instruction. It is three:
1. READ sum from memory into a register
2. ADD a[i] to that register
3. WRITE the register back to sum
TRACE — two threads, sum starts at 100, T0 adds 5, T1 adds 3:
Time Thread 0 Thread 1 sum in memory
t1 reads sum -> 100 100
t2 reads sum -> 100 100
t3 100 + 5 = 105 100
t4 100 + 3 = 103 100
t5 writes 105 105
t6 writes 103 103 <-- WRONG
Expected 108. Got 103. Thread 0's update vanished completely.
This is called a LOST UPDATE, and it is exactly the ATM trace in A6.
The window between reading and writing is where another thread can interfere. An operation that cannot be interrupted in this way is called atomic; sum += a[i] is not atomic.
A critical section is a region of code that must be executed by at most one thread at a time. OpenMP provides #pragma omp critical and #pragma omp atomic:
#pragma omp parallel for
for (i = 0; i < N; i++) {
#pragma omp critical
sum += a[i]; // correct — but now effectively SERIAL
}
This is correct but usually terrible: every iteration now waits for the lock, so the parallel version can be slower than the serial one. Correctness has been bought by destroying the parallelism. The right answer is a reduction.
C5. Reduction — the right fix
THE RIGHT WAY — one word
double sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (i = 0; i < N; i++)
sum += a[i];
What reduction actually does. The compiler gives every thread a private copy of sum, initialised to the identity element of the operator (0 for +, 1 for *). Each thread accumulates into its own copy with no sharing whatsoever, and therefore no contention. At the end of the region the runtime combines the private copies into the original variable, using the same operator, once per thread.
Thread 0: private sum = 0 -> accumulates a[0..249] -> 1200
Thread 1: private sum = 0 -> accumulates a[250..499] -> 1350
Thread 2: private sum = 0 -> accumulates a[500..749] -> 1180
Thread 3: private sum = 0 -> accumulates a[750..999] -> 1270
----
COMBINE at the join: 1200 + 1350 + 1180 + 1270 = 5000
N shared writes became 4 combines. No lock in the hot loop.
Valid reduction operators include + * - & | ^ && || max min.
| Approach | Correct? | Fast? | Notes |
|---|---|---|---|
Plain sum += a[i] | No | — | Race condition; lost updates |
critical | Yes | No | Serialises every iteration |
atomic | Yes | Slightly better | Hardware atomic instruction; still contended |
| Manual private + combine | Yes | Yes | What reduction does, written by hand |
reduction(+:sum) | Yes | Yes | The correct answer |
Note the general principle, which recurs throughout the course: accumulate locally, combine once. The same idea reappears as the fix for false sharing (Unit II), as the parallel reduction tree in CUDA (Unit IV), and as the combiner in MapReduce (Unit V).
C6. Data scoping, and which loops parallelise
| Clause | Meaning |
|---|---|
shared(x) | One copy of x, visible to all threads. The default for variables declared outside the region. |
private(x) | Each thread gets its own uninitialised copy. The loop counter of a parallel for is private automatically. |
firstprivate(x) | Private, but initialised to the value x held before the region. |
lastprivate(x) | Private, and the value from the logically last iteration is copied back out. |
reduction(op:x) | Private copy per thread, combined with op at the end. |
SHARED BY ACCIDENT — the classic bug
int i, j, temp; int i, j, temp;
#pragma omp parallel for #pragma omp parallel for private(j, temp)
for (i = 0; i < N; i++) for (i = 0; i < N; i++)
for (j = 0; j < N; j++) { for (j = 0; j < N; j++) {
temp = a[i][j] * 2; temp = a[i][j] * 2;
b[i][j] = temp; b[i][j] = temp;
} }
i is private (loop counter). j and temp are now private.
j and temp are SHARED — threads Correct.
overwrite each other's values.
Garbage output, different each run.
Will this loop parallelise? A loop is safe to parallelise if its iterations are independent — no iteration reads a value another iteration writes.
| Loop body | Safe? | Reason |
|---|---|---|
c[i] = a[i] + b[i]; | Yes | Every iteration touches a distinct element |
sum += a[i]; | Only with reduction | All iterations write one shared variable |
a[i] = a[i-1] + 1; | No | Loop-carried dependence — iteration i needs the result of i−1 |
if (a[i] > max) max = a[i]; | Only with reduction(max:max) | Shared write; a race otherwise |
printf("%d\n", a[i]); | Runs, but output order is undefined | Not a correctness bug in the data, but the order is non-deterministic |
A loop-carried dependence is the single most common reason a loop cannot be parallelised, and recognising one is a standard exam task.
Quick recall
Everything above, compressed. If you can reconstruct this page you know Unit I.
Formulas
Power P = C x V^2 x f (why the clock stopped rising)
Speedup S = T(1) / T(N)
Perfect speedup S = N on N cores
Amdahl ceiling S_max = 1 / (1 - f) f = parallel fraction
Pipeline time fill + (jobs x slowest stage)
The definitions you must be able to state
- Concurrency — dealing with many things at once (structure). Parallelism — doing many things at once (execution).
- Latency — time for one task. Throughput — tasks per unit time.
- Data parallelism — same operation, different data. Task parallelism — different operations, same time.
- SISD / SIMD / MISD / MIMD — Flynn, by instruction and data streams.
- SPMD — one program, multiple data, branching on rank. A special case of MIMD, not a fifth category.
- UMA — equal access time to all memory. NUMA — local fast, remote slow.
- Race condition — result depends on thread timing. Critical section — code only one thread may execute at a time. Atomic — an operation that cannot be interrupted.
- Fork-join — master forks a team, team executes, threads join at a barrier.
- Loop-carried dependence — iteration i requires a result produced by an earlier iteration; prevents parallelisation.
| Flynn class | Instruction streams | Data streams | Example |
|---|---|---|---|
| SISD | 1 | 1 | Single-core processor |
| SIMD | 1 | Many | GPU, vector unit |
| MISD | Many | 1 | Redundant fault-tolerant systems |
| MIMD | Many | Many | Multicore CPU, cluster |
Self-check
Answer each out loud before opening it. Anything you have to peek at is your revision priority.
1. Why did processor clock speeds stop rising, and what did manufacturers do instead?
2. A program runs on a single-core machine and makes progress on ten downloads at once. Is that concurrency or parallelism?
3. Adding servers behind a load balancer takes a site from 5 to 20 requests/sec, but each page still takes 200 ms. Explain.
4. A 10-hour job has 1 hour that cannot be parallelised. What is the maximum possible speedup, and why does adding cores stop helping?
5. Classify: a GPU, a single-core PC, a 16-core server, a triple-redundant flight computer.
6. Is SPMD a fifth Flynn category?
7. Distinguish UMA from NUMA, and explain why NUMA limits vertical scaling.
8. Compare shared and distributed memory on programmability and scalability.
9. Why does sum += a[i] inside a parallel loop give a different answer every run? Trace it.
10. What does reduction(+:sum) do, and why is it better than critical?
sum initialised to the operator's identity (0 for +). Threads accumulate independently with no sharing, and the private copies are combined once at the join. critical is also correct but serialises every iteration, so the parallel version can be slower than serial; reduction turns N contended writes into a handful of combines and keeps the loop parallel.11. Which of these parallelise safely? c[i]=a[i]+b[i] · a[i]=a[i-1]+1 · sum+=a[i]
reduction(+:sum), since otherwise every iteration writes one shared variable.