Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Read-write lock in Perl

I am looking for a good way to implement read/write lock in Perl. This is needed to synchronize file access from different Perl threads and/or processes on Windows and Unix. Tried Fcntl::flock which would be perfect for me if it worked as expected. Unfortunately it looks like under stress flock allows to set lock on already locked file in another thread. Looked into some CPAN modules, but most are implemented with flock. Next I am planning to evaluate fcntl for Unix and Win32::Mutex for Windows. This seems like a pretty common task and perhaps I am missing some simple solution. If you are aware of any, could you please point it out for me?

Thank you!

like image 629
AndyH Avatar asked Dec 10 '22 09:12

AndyH


1 Answers

The flock won't do what you want across threads.

You can implement your own locking using sysopen, which fails if the file exists when used with O_EXCL|O_CREAT.

An example, with child processes competing for a lock

use warnings;
use strict;
use feature 'say';
use Fcntl;
use Time::HiRes qw(sleep);

my $lock_file = ".lock.$$";
sub get_lock {
    my ($file, $pid) = @_; 
    my $fh;
    while (not sysopen $fh, $file, O_WRONLY|O_EXCL|O_CREAT) {
        say "\t($$: lock-file exists ..)";
        sleep 0.5;
    }   
    say $fh $pid;
}
sub release_lock {
    my ($file, $pid) = @_; 
    unlink $file or die "Error unliking $file: $!";
    say "\t($$: released lock)";
}

my @pids;
for (1..4) {
    my $pid = fork // die "Can't fork: $!";
    if ($pid == 0) {
        sleep rand 1;
        get_lock($lock_file, $$);
        say "$$, locked and processing";
        sleep rand 1;
        release_lock($lock_file, $$);
        say "$$ completed.";
        exit
    }   
    push @pids, $pid;    
}
wait for @pids;

It is better use File::Temp for the lockfile name but read the docs carefully for subtleties.

Sample output with 3 processes

3659, locked and processing
        (3660: lock-file exists ..)
        (3658: lock-file exists ..)
        (3659: released lock)
3659 completed.
3660, locked and processing
        (3658: lock-file exists ..)
        (3658: lock-file exists ..)
        (3660: released lock)
3660 completed.
3658, locked and processing
        (3658: released lock)
3658 completed.

The O_EXCL may be unsuppored under NFS: you must have at least 2.6 kernel and NFSv3 or there will be a race condition. If this is a problem the workaround is to use link(2) to obtain a lock. See man 2 open (also for other details since sysopen uses open syscall).


To lock only file access, for example

sub open_with_lock {
    my ($file, $mode) = @_; 
    get_lock($lock_file, $$);
    open my $fh, $mode, $file or die "Can't open $file: $!";
    return $fh;
}

sub close_and_release {
    my ($fh) = @_; 
    close $fh;
    release_lock($lock_file, $$);
    return 1;
}

These can be placed in a module along with get_lock and release_lock, with the lock-file name as a package global, for example.

A simple test-driver

# use statements as above
use Path::Tiny;           # only to show the file

my $lock_file = ".lock.file.access.$$"; 
my $file = 't_LOCK.txt';    
my @pids;
for (1..4) 
{
    my $pid = fork // die "Can't fork: $!";

    if ($pid == 0) {
        sleep rand 1;
        my $fh = open_with_lock($file, '>>');
        say "$$ (#$_) opening $file ..";
        say $fh "this is $$ (#$_)";
        sleep rand 1;
        close_and_release($fh);
        say "$$ (#$_) closed $file.";
        say '---';
        exit;
    }   
    push @pids, $pid;
}
wait for @pids;

print path($file)->slurp;
unlink $file;

With use statements from the first example and with 3 forks, a run

        (18956: "lock"-file exists ..)    # print out of order
18954 (#1) opening t_LOCK.txt ...
        (18955: "lock"-file exists ..)
        (18956: "lock"-file exists ..)
        (18955: "lock"-file exists ..)
        (18954: released lock)
18954 (#1) closed t_LOCK.txt.
---
18956 (#3) opening t_LOCK.txt ...
        (18955: "lock"-file exists ..)
        (18956: released lock)
18956 (#3) closed t_LOCK.txt.
---
18955 (#2) opening t_LOCK.txt ...
        (18955: released lock)
18955 (#2) closed t_LOCK.txt.
---
this is 18954 (#1)
this is 18956 (#3)
this is 18955 (#2)

(note that independent processes are fighting for STDOUT)

like image 57
zdim Avatar answered Jan 01 '23 16:01

zdim