ZetCode

Perl flock

last modified November 7, 2026

When several processes use the same file at the same time, their reads and writes can interleave in ways that corrupt the data. This happens as soon as a program forks workers, as in the Perl processes and Parallel::ForkManager articles, or when a cron job starts while the previous run is still active. The flock function coordinates such processes by placing a lock on an open file.

The locks are advisory: the operating system does not stop a process from reading or writing a locked file. They only work if every process that uses the file calls flock before touching it. The lock types are constants from the core Fcntl module, imported with the :flock tag:

The examples were run with Perl 5.44 on Linux. They fork child processes, so their timings and process ids vary between runs.

A race condition

The classic example is a counter stored in a file. Each process reads the number, adds one, and writes it back. Five processes each increment the counter 200 times, without any locking.

race.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;

my $file = 'counter.txt';
my ($workers, $rounds) = (5, 200);

open my $init, '>', $file or die "$file: $!";
print $init "0\n";
close $init;

for (1 .. $workers) {
    my $pid = fork // die "fork: $!";
    next if $pid;

    for (1 .. $rounds) {
        open my $fh, '+<', $file or die "$file: $!";
        my $n = <$fh> // 0;
        seek $fh, 0, 0;
        truncate $fh, 0;
        print $fh $n + 1, "\n";
        close $fh;
    }

    exit 0;
}

1 while wait != -1;

open my $fh, '<', $file or die "$file: $!";
chomp(my $n = <$fh> // 0);
say "expected: ", $workers * $rounds;
say "counter:  $n";

The +< mode opens the file for reading and writing without truncating it. After reading, the program rewinds the file, truncates it, and writes the new value. The result should be 1,000, but nearly all increments are lost. Two processes often read the same number and both write the same result. Worse, a process may read the file just after another one has truncated it, find it empty, and start again from zero.

$ perl race.pl
expected: 1000
counter:  7

An exclusive lock

The fix is to make the read-modify-write sequence exclusive. The process takes an exclusive lock right after opening the file and keeps it until it has written the new value.

counter.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);

my $file = 'counter.txt';
my ($workers, $rounds) = (5, 200);

open my $init, '>', $file or die "$file: $!";
print $init "0\n";
close $init;

sub increment {
    open my $fh, '+<', $file or die "$file: $!";
    flock $fh, LOCK_EX or die "flock: $!";

    my $n = <$fh> // 0;
    seek $fh, 0, 0;
    truncate $fh, 0;
    print $fh $n + 1, "\n";

    close $fh or die "$file: $!";
}

for (1 .. $workers) {
    my $pid = fork // die "fork: $!";
    next if $pid;

    increment() for 1 .. $rounds;
    exit 0;
}

1 while wait != -1;

open my $fh, '<', $file or die "$file: $!";
chomp(my $n = <$fh> // 0);
say "expected: ", $workers * $rounds;
say "counter:  $n";

The flock $fh, LOCK_EX call blocks until no other process holds a lock on the file. Closing the handle flushes the buffered output and then releases the lock, in that order, so the next process always sees the complete new value. An explicit LOCK_UN is not needed; Perl flushes the handle before it unlocks, so it would be safe, but close does both in one step. Note also that the file is opened with +<, not with >; opening with > truncates the file before the lock is obtained, while another process may still be reading it.

$ perl counter.pl
expected: 1000
counter:  1000

Shared and exclusive locks

Readers do not disturb each other, so they can share a lock. A writer needs an exclusive lock, which it gets only when no reader holds a shared one. The next program starts three readers and one writer with small delays and logs what happens.

shared.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);
use Time::HiRes qw(time sleep);

$| = 1;

my $file = 'data.txt';
my $start = time;

open my $init, '>', $file or die "$file: $!";
print $init "version 1\n";
close $init;

sub logmsg { printf "%4.1f s  %-8s %s\n", time - $start, @_ }

sub reader {
    my ($name, $delay) = @_;
    sleep $delay;

    open my $fh, '<', $file or die "$file: $!";
    logmsg($name, 'waits for shared lock');
    flock $fh, LOCK_SH or die "flock: $!";

    chomp(my $content = <$fh>);
    logmsg($name, "reads '$content'");
    sleep 1;
    close $fh;
}

sub writer {
    my ($name, $delay) = @_;
    sleep $delay;

    open my $fh, '+<', $file or die "$file: $!";
    logmsg($name, 'waits for exclusive lock');
    flock $fh, LOCK_EX or die "flock: $!";

    logmsg($name, 'writes version 2');
    seek $fh, 0, 0;
    truncate $fh, 0;
    print $fh "version 2\n";
    sleep 1;
    close $fh;
    logmsg($name, 'released');
}

my @jobs = (
    [\&reader, 'reader1', 0],
    [\&reader, 'reader2', 0.2],
    [\&writer, 'writer',  0.4],
    [\&reader, 'reader3', 0.6],
);

for my $job (@jobs) {
    my ($code, @args) = @$job;
    my $pid = fork // die "fork: $!";
    next if $pid;
    $code->(@args);
    exit 0;
}

1 while wait != -1;

Each reader holds its shared lock for one second. The first two readers get their locks immediately. The writer arrives at 0.4 s and has to wait. The third reader arrives while the writer is waiting and still gets its shared lock at once, because shared locks are compatible with each other. The writer obtains the exclusive lock only at 1.6 s, after the last reader has finished. On Linux, a waiting writer does not block new readers, so a steady stream of readers can starve a writer; applications with heavy read traffic need to account for that. Every reader sees a consistent version of the file.

$ perl shared.pl
 0.0 s  reader1  waits for shared lock
 0.0 s  reader1  reads 'version 1'
 0.2 s  reader2  waits for shared lock
 0.2 s  reader2  reads 'version 1'
 0.4 s  writer   waits for exclusive lock
 0.6 s  reader3  waits for shared lock
 0.6 s  reader3  reads 'version 1'
 1.6 s  writer   writes version 2
 2.6 s  writer   released

Non-blocking locks: a single instance

With LOCK_NB, flock does not wait; it returns false if the lock is held by another process. A common use is to make sure only one copy of a script runs at a time, for example a cron job.

single.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);

my $lockfile = '/tmp/single-demo.lock';

open my $lock, '>>', $lockfile or die "$lockfile: $!";

unless (flock $lock, LOCK_EX | LOCK_NB) {
    say "[$$] another instance is running, exiting";
    exit 1;
}

say "[$$] got the lock, working";
sleep 2;
say "[$$] done";

The lock file is opened in append mode, which creates it if needed and never truncates it. The script keeps the handle in a lexical variable for its whole run, so the lock lasts until the process exits; the operating system releases it even if the process crashes, which is the big advantage over PID files. We start one instance in the background and a second one half a second later; the second instance gives up immediately.

$ perl single.pl & sleep 0.5; perl single.pl; wait
[22558] got the lock, working
[22560] another instance is running, exiting
[22558] done

Waiting with a timeout

A blocking flock may wait forever. Combining LOCK_NB with a retry loop gives a lock with a timeout.

timeout.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);
use Time::HiRes qw(sleep time);

my $file = 'job.lock';

sub lock_with_timeout {
    my ($fh, $seconds) = @_;
    my $deadline = time + $seconds;

    until (flock $fh, LOCK_EX | LOCK_NB) {
        return 0 if time >= $deadline;
        sleep 0.1;
    }

    return 1;
}

my $pid = fork // die "fork: $!";

if ($pid == 0) {
    open my $fh, '>>', $file or die "$file: $!";
    flock $fh, LOCK_EX or die "flock: $!";
    sleep 2.5;
    exit 0;
}

sleep 0.2;
open my $fh, '>>', $file or die "$file: $!";

for my $timeout (1, 3) {
    my $t0 = time;
    my $ok = lock_with_timeout($fh, $timeout);
    printf "timeout %d s: %s after %.1f s\n", $timeout, $ok ? 'locked' : 'gave up', time - $t0;
}

waitpid $pid, 0;

The child process holds the lock for 2.5 seconds. The parent first tries with a one-second timeout and gives up. The second attempt, with three seconds, succeeds as soon as the child exits and the operating system releases its lock. Polling every 0.1 seconds is simple and portable; the alternative is a blocking flock interrupted by an alarm signal, as shown in the Perl signals article.

$ perl timeout.pl
timeout 1 s: gave up after 1.0 s
timeout 3 s: locked after 1.3 s

Locks across fork

A lock created by flock belongs to the open file description, not to the process. A child created by fork inherits the parent's file handles, and with them the lock.

inherit.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);

my $file = 'inherit.lock';

open my $fh, '>>', $file or die "$file: $!";
flock $fh, LOCK_EX or die "flock: $!";
say 'parent holds the lock';

my $pid = fork // die "fork: $!";

if ($pid == 0) {
    my $same = flock $fh, LOCK_EX | LOCK_NB;
    say 'child, inherited handle: ', $same ? 'lock granted' : 'blocked';

    open my $own, '>>', $file or die "$file: $!";
    my $other = flock $own, LOCK_EX | LOCK_NB;
    say 'child, own handle:       ', $other ? 'lock granted' : 'blocked';
    exit 0;
}

waitpid $pid, 0;

The child's request through the inherited handle succeeds, because it refers to the same open file and therefore to the same lock. When the child opens the file itself, it gets a new open file description, and the lock request is refused because the parent still holds the lock. The practical rule is that each worker should open the file itself after the fork, and should not rely on a handle opened by the parent.

$ perl inherit.pl
parent holds the lock
child, inherited handle: lock granted
child, own handle:       blocked

Logging from Parallel::ForkManager workers

A frequent case is a pool of workers that write their results to a common log file. When a record consists of several writes, records from different workers can interleave.

pfm.pl
#!/usr/bin/perl

use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:flock);
use IO::Handle;
use Time::HiRes qw(sleep);
use Parallel::ForkManager;

my $use_lock = (shift // '') eq 'lock';
my $logfile = 'report.log';
unlink $logfile;

my $pm = Parallel::ForkManager->new(8);

for my $job (1 .. 40) {
    $pm->start and next;

    open my $log, '>>', $logfile or die "$logfile: $!";
    $log->autoflush(1);

    flock $log, LOCK_EX or die "flock: $!" if $use_lock;

    print $log "begin job $job\n";
    sleep 0.01;
    print $log "  result: ", $job * $job, "\n";
    print $log "end job $job\n";

    close $log;
    $pm->finish;
}

$pm->wait_all_children;

open my $fh, '<', $logfile or die "$logfile: $!";
my @lines = <$fh>;

my $broken = 0;
for (my $i = 0; $i < @lines; $i += 3) {
    my ($job) = $lines[$i] =~ /^begin job (\d+)/;
    $broken++ unless defined $job
        && ($lines[$i + 2] // '') eq "end job $job\n";
}

printf "%s: %d lines, %d broken records\n",
    $use_lock ? 'with flock' : 'without lock', scalar @lines, $broken;

Eight workers run 40 jobs. Each worker opens the log in append mode, turns on autoflush so that each print goes to the file immediately, and writes a record of three lines. The short sleep between the lines stands for real work. Without the lock, all workers write their first lines before any of them finishes, and every record is broken. With the lock argument, each worker holds an exclusive lock for its whole record, and the log is intact. The verification loop checks that each begin line is followed by the matching end line.

$ perl pfm.pl; head -6 report.log
without lock: 120 lines, 40 broken records
begin job 1
begin job 2
begin job 3
begin job 4
begin job 5
begin job 6
$ perl pfm.pl lock; head -6 report.log
with flock: 120 lines, 0 broken records
begin job 1
  result: 1
end job 1
begin job 2
  result: 4
end job 2

Pitfalls

Best practices

In this article we have used flock to fix a race condition, combined shared and exclusive locks, written a single-instance script with LOCK_NB, implemented a lock timeout, examined how locks behave across fork, and kept a log file consistent while it was written by Parallel::ForkManager workers.

Source

perlfunc: flock · perlopentut: File locking · perlfaq5: How can I lock a file?

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 Perl tutorials.