You might use an elaborate cooperation protocol such as in the following. Both ends, reader and writer, use common code in the TakeTurns module that handles fussy details such as locking and where the lock file lives. The clients need only specify what they want to do when they have exclusive access to the file.
reader
#! /usr/bin/perl
use warnings;
use strict;
use TakeTurns;
my $runs = 0;
reader "file.txt" =>
       sub {
         my($fh) = @_;
         my @lines = <$fh>;
         print map "got: $_", @lines;
         ++$runs <= 10;
       };
writer
#! /usr/bin/perl
use warnings;
use strict;
use TakeTurns;
my $n = 10;
my @chars = ('a'..'z','A'..'Z','0'..'9','_');
writer "file.txt" =>
       sub { my($fh) = @_;
             print $fh join("" => map $chars[rand @chars], 1..$n), "\n"
               or warn "$0: print: $!";
           };
The TakeTurns module is execute-around at work:
package TakeTurns;                               
use warnings;                                    
use strict;                                      
use Exporter 'import';                           
use Fcntl qw/ :DEFAULT :flock /;                 
our @EXPORT = qw/ reader writer /;               
my $LOCKFILE = "/tmp/taketurns.lock";            
sub _loop ($&) {
  my($path,$action) = @_;
  while (1) {
    sysopen my $lock, $LOCKFILE, O_RDWR|O_CREAT
                                   or die "sysopen: $!";
    flock $lock, LOCK_EX           or die "flock: $!";
    my $continue = $action->();
    close $lock                    or die "close: $!";
    return unless $continue;
    sleep 0;
  }
}
sub writer {
  my($path,$w) = @_;
  _loop $path =>
        sub {
          open my $fh, ">", $path   or die "open $path: $!";
          my $continue = $w->($fh);
          close $fh                 or die "close $path: $!";
          $continue;
        };
}
sub reader {
  my($path,$r) = @_;
  _loop $path =>
        sub {
          open my $fh, "<", $path        or die "open $path: $!";
          my $continue = $r->($fh);
          close $fh                      or die "close $path: $!";
          $continue;
        };
}
1;
Sample output:
got: 1Upem0iSfY
got: qAALqegWS5
got: 88RayL3XZw
got: NRB7POLdu6
got: IfqC8XeWN6
got: mgeA6sNEpY
got: 2TeiF5sDqy
got: S2ksYEkXsJ
got: zToPYkGPJ5
got: 6VXu6ut1Tq
got: ex0wYvp9Y8
Even though you went to so much trouble, there are still issues. The protocol is unreliable, so reader has no guarantee of seeing all messages that writer sends. With no writer active, reader is content to read the same message over and over.
You could add all this, but a more sensible approach would be using abstractions the operating system provides already.
For example, Unix named pipes seem to be a pretty close match to what you want, and note how simple the code is:
pread
#! /usr/bin/perl
use warnings;
use strict;
my $pipe = "/tmp/mypipe";
system "mknod $pipe p 2>/dev/null";
open my $fh, "<", $pipe or die "$0: open $pipe: $!";
while (<$fh>) {
  print "got: $_";
  sleep 0;
}
pwrite
#! /usr/bin/perl
use warnings;
use strict;
my $pipe = "/tmp/mypipe";
system "mknod $pipe p 2>/dev/null";
open my $fh, ">", $pipe or die "$0: open $pipe: $!";
my $n = 10;
my @chars = ('a'..'z','A'..'Z','0'..'9','_');
while (1) {
  print $fh join("" => map $chars[rand @chars], 1..$n), "\n"
    or warn "$0: print: $!";
}
Both ends attempt to create the pipe using mknod because they have no other method of synchronization. At least one will fail, but we don't care as long as the pipe exists.
As you can see, all the waiting machinery is handled by the system, so you do what you care about: reading and writing messages.