Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lock.protect and callsame

I'm trying to write a trait to threadsafe a sub automagically. This is what I've got:

#| A trait to ensure that a sub is not run on multiple threads simultaneously.
multi sub trait_mod:<is> (Sub \code, :$protected!) {

    # If :!protected, do nothing.
    if $protected {

        # Create a new lock outside the multithreaded area
        my $lock = Lock.new;

        # Wrap the sub with a routine that hides callsame in a lock
        code.wrap: sub (|) {
            $lock.protect: {callsame}
        }
    }
}

#| Should print "Start X and finish X" if properly protected
sub needs-protection($x) is protected {
   print "Start $x and ";
   sleep 1;
   say "finish $x";
}

# Test it out.
# If not protected, completes in 1 second with malformed output
(1..4).hyper(:1batch, :4degree) { 
   needs-protection $_
}

However, AFAICT, it seems that the callsame isn't doing anything (it returns Nil but that's it). My guess is that it's somehow attempting to call a different candidate for .protect, but I don't see a way to ensure that the callsame is linked to the wrapped sub, rather than some other method.

I was able to get it to work by doing

multi sub trait_mod:<is> (Sub \code, :$protected!) {
    if $protected {
        my $lock = Lock.new;
        code.wrap: sub (|c) {
            if CALLERS::<$*PROTECTED> {
                $*PROTECTED = False;
                return callsame;
            }

            $lock.protect: { 
                my $*PROTECTED = True;
                code.CALL-ME(|c);
            }
        }
    }
}

But that feels cludgy and I'm probably missing something that allows a True value for $*PROTECTED to slip out when things aren't safe. Is there a way to make a direct callsame while inside of a protect-ed block?

like image 637
user0721090601 Avatar asked Feb 20 '21 18:02

user0721090601


1 Answers

Deferral routines like callsame look for the nearest dynamically scoped dispatch to resume. A block {callsame} passed to the method protect will be called by the protect method, and the nearest dispatch in dynamic scope will be the method dispatch to protect. Therefore, it will attempt to defer to a protect method in a base class of Lock. There aren't any, thus the Nil result.

To solve this, we need to obtain the wrapped target in the correct dynamic scope, and make it available lexically. This can be achieved using nextcallee:

#| A trait to ensure that a sub is not run on multiple threads simultaneously.
multi sub trait_mod:<is> (Sub \code, :$protected!) {
    # If :!protected, do nothing.
    if $protected {

        # Create a new lock outside the multithreaded area
        my $lock = Lock.new;

        # Wrap the sub with a routine that hides callsame in a lock
        code.wrap: sub (|c) {
            my &target = nextcallee;
            $lock.protect: { target(|c) }
        }
    }
}

#| Should print "Start X and finish X" if properly protected
sub needs-protection($x) is protected {
   print "Start $x and ";
   sleep 1;
   say "finish $x";
}

# Test it out.
# If not protected, completes in 1 second with malformed output
for (1..4).hyper(:1batch, :4degree) { 
   needs-protection $_
}

This gives the output that I expect you are expecting.

like image 99
Jonathan Worthington Avatar answered Oct 05 '22 11:10

Jonathan Worthington