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:
LOCK_SH, a shared lock, which many processes may hold at once, typically for reading;LOCK_EX, an exclusive lock, which only one process may hold, for writing;LOCK_UN, which releases a lock;LOCK_NB, which is combined with the others to return immediately instead of waiting.
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.
#!/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.
#!/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.
#!/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.
#!/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.
#!/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.
#!/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.
#!/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
- Locks are advisory: a program that does not call
flockcan still read and write the file. - Network file systems:
flockmay be emulated or unreliable on NFS and similar file systems; keep lock files on local disks. - Replacing files: a lock is attached to a file, not to a name.
If a writer replaces a file with
rename, processes locking the old file and the new file do not exclude each other. Use a separate, permanent lock file to protect such updates. - Different locking APIs:
flocklocks andfcntlrecord locks are independent on Linux; all programs sharing a file must use the same mechanism. - Closing a handle releases the lock: a handle that goes out of scope at the end of a block releases its lock, too.
Best practices
- Lock before reading: take the lock immediately after
openand before the first read. - Never open with > before locking: use
+<or>>, lock, and only then truncate. - Release by closing: let
closeflush the data and release the lock. - Hold locks briefly: do the slow work before taking the lock and only the file update while holding it.
- Use LOCK_NB for single instances: a lock file with a non-blocking exclusive lock is more robust than a PID file.
- Open after fork: each worker should open and lock the file on its own.
- Check the return value:
flockreturns false on failure; always handle it.
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
List all Perl tutorials.