ZetCode

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.

omp1.f90
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.

omp2.f90
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.

omp3.f90
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.

omp4.f90
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.

omp5.f90
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.

omp6.f90
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.

omp7.f90
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

OpenMP specification

GNU libgomp manual

GNU Fortran manual: OpenMP

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.