OpenMP with gfortran
last modified November 14, 2026
OpenMP is a standard for shared-memory parallelism: the program runs
in several threads that see the same memory, and the programmer marks the
places where the work may be divided with comments that begin with
!$omp. A compiler that does not know OpenMP treats those lines as
ordinary comments and produces a serial program; gfortran with
-fopenmp turns them into calls into a thread library and links
libgomp. The thread library is queried through the module
omp_lib.
This article starts a parallel region and counts its threads, sums a million numbers with a reduction, passes private and first-private values into the iterations, shows what happens to a counter that is updated without protection, compares static and dynamic scheduling on an uneven workload, measures the speedup of a compute-heavy loop at one, two, four and eight threads, and controls the thread count from the source and from the environment.
The parallel region
!$omp parallel forks a team of threads, each of which executes the
block that follows, and !$omp end parallel joins them again.
Inside the block, omp_get_thread_num() returns the number of the
current thread, counting from zero, and omp_get_num_threads() the
size of the team. The thread that executed the region before the fork is the
master and keeps the number zero.
program omp1
use omp_lib
implicit none
integer, parameter :: max_threads = 16
integer :: ids(max_threads), nthreads
ids = -1
nthreads = 0
!$omp parallel
ids(omp_get_thread_num() + 1) = omp_get_thread_num()
nthreads = omp_get_num_threads()
!$omp end parallel
print '(a, i0)', 'threads in the region: ', nthreads
print '(a, i0)', 'the maximum is: ', omp_get_max_threads()
print '(a, 8i3)', 'the ids they recorded: ', ids(1:nthreads)
print '(a, l1)', 'the main thread is id 0: ', ids(1) == 0
end program omp1
Each thread writes its number into the element of the array that belongs to it, which is a race-free way to collect per-thread information; after the region ends there is an implicit barrier, so the master can print the whole array. The order in which the threads run is not defined, but the contents of the array are: eight threads numbered zero to seven, with the master among them. The array is larger than any likely team so that the program does not depend on the machine.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp1.f90 -o omp1 $ ./omp1 threads in the region: 8 the maximum is: 8 the ids they recorded: 0 1 2 3 4 5 6 7 the main thread is id 0: T
A loop divided among the threads
!$omp parallel do is the combination that matters most: the
iterations of the loop are distributed over a team, and the loop index is made
private automatically. Results that each iteration computes for itself need no
attention, but a variable that every iteration adds to needs a
reduction, declared with reduction(+:variable). OpenMP
gives each thread a private copy of the variable, initialised to the identity of
the operator, and combines the copies at the end.
program omp2
use omp_lib
use iso_fortran_env, only: int64, real64
implicit none
integer, parameter :: n = 1000000
integer :: i
integer(int64) :: serial, parallel
real(real64) :: t0, t1
serial = 0
do i = 1, n
serial = serial + int(i, int64)
end do
parallel = 0
t0 = omp_get_wtime()
!$omp parallel do reduction(+:parallel)
do i = 1, n
parallel = parallel + int(i, int64)
end do
!$omp end parallel do
t1 = omp_get_wtime()
print '(a, i0)', 'the serial sum: ', serial
print '(a, i0)', 'the parallel sum: ', parallel
print '(a, l1)', 'they agree: ', serial == parallel
print '(a, f0.4, a)', 'the parallel loop took ', t1 - t0, ' s'
end program omp2
The sum of the first million integers is 500000500000, which needs a 64-bit
integer to hold; the reduction computes it without a lock in the inner loop,
because each thread accumulates into its own copy. The two totals are equal, and
the loop takes well under a millisecond, so at this size the threads are mostly
overhead. A reduction is also available for *, min,
max, .and. and .or..
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp2.f90 -o omp2 $ ./omp2 the serial sum: 500000500000 the parallel sum: 500000500000 they agree: T the parallel loop took .0006 s
Private, first private and last private
A variable declared outside a region is shared by default, and sharing a
temporary is a mistake: two threads overwrite each other's intermediate value.
The private clause gives every thread its own uninitialised copy,
firstprivate gives every thread a copy that starts with the value
the variable had before the region, and lastprivate copies the
value from the iteration that would have run last back out to the shared
variable.
program omp3
implicit none
integer, parameter :: n = 8
integer :: i, t, start, last, b(n), check(n)
start = 100
last = 0
b = 0
do i = 1, n
check(i) = start + i * 10 + 3
end do
!$omp parallel do private(t) firstprivate(start) lastprivate(last)
do i = 1, n
t = i * 10 + 3
b(i) = start + t
last = i
end do
!$omp end parallel do
print '(a, 8i5)', 'the results: ', b
print '(a, i0)', 'the largest difference from the serial version: ', &
maxval(abs(b - check))
print '(a, i0)', 'lastprivate carried out of the loop: ', last
end program omp3
The temporary t is private, so each iteration computes it without
interference, and the results match the serial values exactly. The starting
value 100 is first-private, which would matter if the loop changed it, and the
index of the final iteration leaves the loop in last. Without the
private clause the same program would compile and usually print
numbers that look plausible, which is the danger: an accidental share does not
produce an error, it produces a wrong answer now and then.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp3.f90 -o omp3 $ ./omp3 the results: 113 123 133 143 153 163 173 183 the largest difference from the serial version: 0 lastprivate carried out of the loop: 8
Critical sections and atomic updates
When a variable really must be shared, the update has to be protected.
!$omp critical allows one thread at a time to execute a block, and
!$omp atomic update does the same for a single statement, at a
lower cost. The program below counts two hundred thousand iterations three ways:
without protection, inside a critical section, and with an atomic update.
program omp4
use iso_fortran_env, only: int64
implicit none
integer, parameter :: n = 200000
integer :: i
integer(int64) :: plain, guarded, atomic
plain = 0
!$omp parallel do
do i = 1, n
plain = plain + 1
end do
!$omp end parallel do
guarded = 0
!$omp parallel do
do i = 1, n
!$omp critical
guarded = guarded + 1
!$omp end critical
end do
!$omp end parallel do
atomic = 0
!$omp parallel do
do i = 1, n
!$omp atomic update
atomic = atomic + 1
end do
!$omp end parallel do
print '(a, i0)', 'the expected total: ', n
print '(a, i0)', 'without protection: ', plain
print '(a, i0)', 'inside a critical region: ', guarded
print '(a, i0)', 'with an atomic update: ', atomic
end program omp4
The unprotected counter ends far short of the expected total, often below half of it, because the read, the addition and the write of one thread interleave with those of another and increments are overwritten. The exact number changes from run to run, which is the signature of a race. Both protected versions reach exactly two hundred thousand, and the critical section is the slower of the two because it has to acquire a lock for every iteration.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp4.f90 -o omp4 $ ./omp4 the expected total: 200000 without protection: 27120 inside a critical region: 200000 with an atomic update: 200000
Static, dynamic and uneven work
The default schedule divides the iterations into equal blocks, one per thread,
which is ideal when every iteration costs the same. When the cost grows with the
index, the thread that receives the last block does much more work than the
others and the team waits for it. schedule(dynamic, chunk) hands out
chunks on demand instead, so a thread that finishes early takes the next piece;
the price is the bookkeeping of a shared counter.
program omp5
use omp_lib
use iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 200000
integer :: i, j, iterations
real(real64) :: x, static_total, dynamic_total, t0, t1
real(real64) :: static_time, dynamic_time
t0 = omp_get_wtime()
static_total = 0.0_real64
!$omp parallel do reduction(+:static_total) schedule(static) &
!$omp& private(iterations, x, j)
do i = 1, n
iterations = i / 100 + 1
x = 0.0_real64
do j = 1, iterations
x = x + sqrt(real(i * j, real64))
end do
static_total = static_total + x
end do
!$omp end parallel do
t1 = omp_get_wtime()
static_time = t1 - t0
t0 = omp_get_wtime()
dynamic_total = 0.0_real64
!$omp parallel do reduction(+:dynamic_total) schedule(dynamic, 1000) &
!$omp& private(iterations, x, j)
do i = 1, n
iterations = i / 100 + 1
x = 0.0_real64
do j = 1, iterations
x = x + sqrt(real(i * j, real64))
end do
dynamic_total = dynamic_total + x
end do
!$omp end parallel do
t1 = omp_get_wtime()
dynamic_time = t1 - t0
print '(a, f7.4, a)', 'static schedule: ', static_time, ' s'
print '(a, f7.4, a)', 'dynamic schedule: ', dynamic_time, ' s'
print '(a, f0.2, a)', 'the ratio: ', static_time / dynamic_time, 'x'
print '(a, es16.8)', 'the static total: ', static_total
print '(a, es16.8)', 'the dynamic total:', dynamic_total
print '(a, es10.2)', 'their difference: ', abs(static_total - dynamic_total)
end program omp5
The work per iteration grows from one square root to two thousand, and the dynamic schedule finishes in about three quarters of the time, because it does not leave the expensive tail to one thread. The two totals differ only in the last digits, which is the rounding of a different summation order, not an error. Choosing a chunk that is too small makes the scheduling overhead visible; a thousand iterations per chunk is a reasonable starting point for this size of problem.
$ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp5.f90 -o omp5 $ ./omp5 static schedule: 0.1088 s dynamic schedule: 0.0849 s the ratio: 1.28x the static total: 1.77978907E+12 the dynamic total: 1.77978907E+12 their difference: 2.93E-03
Measuring the speedup
A useful parallel loop has to do enough work per iteration to pay for the
threads. The program below performs two hundred and fifty million square roots in a
reduction and repeats the measurement with one, two, four and eight threads,
using omp_get_wtime(), a wall clock with microsecond resolution
that is meant for exactly this purpose. The serial version runs first as the
baseline.
program omp6
use omp_lib
use iso_fortran_env, only: real64
implicit none
integer, parameter :: n = 500000, inner = 500
integer, parameter :: counts(4) = [1, 2, 4, 8]
real(real64) :: total, t0, t1, serial_time, serial_total
integer :: i, j, k, threads
t0 = omp_get_wtime()
serial_total = 0.0_real64
do i = 1, n
do j = 1, inner
serial_total = serial_total + sqrt(real(i * j, real64))
end do
end do
t1 = omp_get_wtime()
serial_time = t1 - t0
print '(a, f7.4, a, es14.6)', 'serial: ', serial_time, ' s ', &
serial_total
do k = 1, size(counts)
threads = counts(k)
total = 0.0_real64
t0 = omp_get_wtime()
!$omp parallel do reduction(+:total) num_threads(threads)
do i = 1, n
do j = 1, inner
total = total + sqrt(real(i * j, real64))
end do
end do
!$omp end parallel do
t1 = omp_get_wtime()
print '(a, i2, a, f7.4, a, f6.2, a)', 'threads ', threads, ': ', &
t1 - t0, ' s speedup ', serial_time / (t1 - t0), 'x'
end do
print '(a, es10.2)', 'the difference from the serial total: ', &
abs(total - serial_total)
end program omp6
Two threads halve the time, four reach about 3.7 times the serial speed, and eight are slower than four, because this machine has four physical cores and eight logical processors and the extra threads compete for the same units. The curve is the usual shape: near-linear while the cores are real, then flat or worse. The two totals differ in the thirteenth digit because a sum of floating point numbers depends on the order of the additions, and the reduction adds them in a different order than the serial loop.
$ gfortran -O2 -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp6.f90 -o omp6 $ OMP_PROC_BIND=true OMP_PLACES=cores ./omp6 serial: 0.3682 s 1.759410E+12 threads 1: 0.3657 s speedup 1.01x threads 2: 0.1894 s speedup 1.94x threads 4: 0.0995 s speedup 3.70x threads 8: 0.1485 s speedup 2.48x the difference from the serial total: 3.04E+00
Controlling the number of threads
Three mechanisms set the size of a team, and they take precedence in a fixed
order. The environment variable OMP_NUM_THREADS sets the default at
start-up, omp_set_num_threads(n) changes that default from inside
the program, and the clause num_threads(n) on a region overrides
both for that region only. The maximum reported by
omp_get_max_threads() follows the same rules.
program omp7
use omp_lib
implicit none
integer :: nthreads
print '(a, i0)', 'the default maximum: ', omp_get_max_threads()
call omp_set_num_threads(3)
nthreads = 0
!$omp parallel
nthreads = omp_get_num_threads()
!$omp end parallel
print '(a, i0)', 'after omp_set_num_threads(3): ', nthreads
print '(a, i0)', 'the maximum is now: ', omp_get_max_threads()
nthreads = 0
!$omp parallel num_threads(5)
nthreads = omp_get_num_threads()
!$omp end parallel
print '(a, i0)', 'the clause num_threads(5): ', nthreads
nthreads = 0
!$omp parallel
nthreads = omp_get_num_threads()
!$omp end parallel
print '(a, i0)', 'back to the default: ', nthreads
print '(a, i0)', 'the thread limit: ', omp_get_thread_limit()
end program omp7
Run without the variable, the program starts with the machine's eight
processors; run with OMP_NUM_THREADS=2, it starts with two. In both
runs the call to omp_set_num_threads(3) raises the default to
three, the clause creates five threads for one region, and the next region
returns to the default rather than to the environment value, because the call
changed it. The thread limit is the largest team the runtime will ever create,
and it is effectively unlimited here.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fopenmp omp7.f90 -o omp7 $ ./omp7 the default maximum: 8 after omp_set_num_threads(3): 3 the maximum is now: 3 the clause num_threads(5): 5 back to the default: 3 the thread limit: 2147483647 $ OMP_NUM_THREADS=2 ./omp7 the default maximum: 2 after omp_set_num_threads(3): 3 the maximum is now: 3 the clause num_threads(5): 5 back to the default: 3 the thread limit: 2147483647
Next steps
The parallel loops article covers
do concurrent, the promise of independence that OpenMP turns into
threads, and the coarrays article reaches
beyond one processor to several images of the program. The
loops article explains the counted
do that every directive here decorates, the
error handling article explains the exit
status of a failed run, and the
scientific simulations article
contains loops of the size where threads begin to pay.
Source
In this article we have started a parallel region and numbered its threads,
summed a million integers with a reduction, passed private, first-private and
last-private variables into a loop, watched an unprotected counter lose most of
its increments and recovered the exact total with a critical section and with an
atomic update, compared static and dynamic scheduling on an uneven workload,
measured the speedup at four team sizes, and set the thread count from the
environment, from a call and from a clause. The previous article,
Fortran parallel loops, shows why the
plain do concurrent construct does not produce any of this on its
own.
Author
List all Fortran tutorials.