Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concurrency, react-ing to more than one supply at a time

Tags:

raku

rakudo

Please consider the code below. Why is the output of this is "BABABA" and not "AAABAA" / "AABAAAB"? Shouldn't the two supplies run in parallel and the whenever fire immedeatly when there is an event in any of them?

my $i = 0; 
my $supply1 = supply { loop { await Promise.in(3); done if $i++> 5; emit("B"); } };
my $supply2 = supply { loop { await Promise.in(1); done if $i++> 5; emit("A"); } };

react 
{ 
    #whenever Supply.merge($supply1, $supply2) -> $x { $x.print }
    whenever $supply1 -> $x { $x.print };
    whenever $supply2 -> $x { $x.print };
}
like image 972
Holli Avatar asked Aug 13 '19 23:08

Holli


3 Answers

When we subscribe to a supply block, the body of that supply block is run immediately in order to set up subscriptions. There's no concurrency introduced as part of this; if we want that, we need to ask for it.

The best solution depends on how close the example is to what you're doing. If it's very close - and you want to emit values every time interval - then the solution is to use Supply.interval instead:

my $i = 0; 
my $supply1 = supply { whenever Supply.interval(3, 3) { done if $i++ > 5; emit("B"); } };
my $supply2 = supply { whenever Supply.interval(1, 1) { done if $i++> 5; emit("A"); } };

react { 
    whenever $supply1 -> $x { $x.print };
    whenever $supply2 -> $x { $x.print };
}

Which simply sets up a subscription and gets out of the setup, and so gives the output you want, however you do have a data race on the $i.

The more general pattern is to just do anything that gets the loop happening out of the setup step. For example, we could use an a kept Promise to just "thunk" it:

my constant READY = Promise.kept;
my $i = 0;
my $supply1 = supply whenever READY {
    loop { await Promise.in(3); done if $i++> 5; emit("B"); }
}
my $supply2 = supply whenever READY {
    loop { await Promise.in(1); done if $i++> 5; emit("A"); }
}

react { 
    whenever $supply1 -> $x { $x.print };
    whenever $supply2 -> $x { $x.print };
}

Which helps because the result of a Promise will be delivered to the supply block via the thread pool scheduler, thus forcing the execution of the content of the whenever - containing the loop - into its own scheduled task.

This isn't especially pretty, but if we define a function to do it:

sub asynchronize(Supply $s) {
    supply whenever Promise.kept {
        whenever $s { .emit }
    }
}

Then the original program only needs the addition of two calls to it:

my $i = 0;
my $supply1 = supply { loop { await Promise.in(3); done if $i++> 5; emit("B") } }
my $supply2 = supply { loop { await Promise.in(1); done if $i++> 5; emit("A") } }

react { 
    whenever asynchronize $supply1 -> $x { $x.print }
    whenever asynchronize $supply2 -> $x { $x.print }
}

To make it work as desired. Arguably, something like this should be provided as a built-in.

It is possible to use a Channel as well, as the other solution proposes, and depending on the problem at hand that may be suitable; the question is a bit too abstracted from a real problem for me to say. This solution stays within the Supply paradigm, and is neater in that sense.

like image 60
Jonathan Worthington Avatar answered Nov 05 '22 22:11

Jonathan Worthington


Ok, so here is my real code. It seems to work, but I think there is a race condition somewhere. Here's some typical (albeit short) output.

A monster hatched.
A monster hatched.
A hero was born.
The Monster is at 2,3
The Monster is at 3,2
The Player is at 0,0
The Monster (2) attacks the Player (3)
The Monster rolls 14
The Player rolls 4
The Monster inflicts 4 damage
The Player (3) attacks the Monster (2)
The Player rolls 11
The Monster rolls 8
The Player inflicts 45 damage
The Monster is dead
The Monster is at -3,-3
The Player is at 4,-3
The Monster (1) attacks the Player (3)
The Monster rolls 8
The Player rolls 5
The Monster inflicts 11 damage
The Player has 32 hitpoints left
The Monster is at -4,1
The Player is at -1,4
The Player (3) attacks the Monster (1)
The Player rolls 12
The Monster rolls 11
The Player inflicts 46 damage
The Monster is dead
Stopping
Game over. The Player has won

Now the strange thing is, sometimes, in maybe 20% of the runs, the last line of the output is

Game over. The GameObject has won 

as if the object got caught while it already is partially deconstructed? Or something? Anyway here's the code.

class GameObject
{
    has Int $.id;
    has Int $.x is rw;
    has Int $.y is rw;
    has $.game;
    has Int $.speed; #the higher the faster
    has Bool $.stopped is rw;

    multi method start( &action )
    {
        start {
            loop {
                &action();
                last if self.stopped;
                await Promise.in( 1 / self.speed );
            }
            $.game.remove-object( self );
        }
    }

    method speed {
        $!speed + 
            # 33% variation from the base speed in either direction
            ( -($!speed / 3).Int .. ($!speed / 3).Int ).pick
            ;
    }
}

role UnnecessaryViolence
{
    has $.damage;
    has $.hitpoints is rw;
    has $.offense;
    has $.defense;

    method attack ( GameObject $target )
    {
        say "The {self.WHAT.perl} ({self.id}) attacks the {$target.WHAT.perl} ({$target.id})";

        my $attacker = roll( $.offense, 1 .. 6 ).sum;
        say "The {self.WHAT.perl} rolls $attacker";

        my $defender = roll( $target.defense, 1 .. 6 ).sum;
        say "The {$target.WHAT.perl} rolls $defender";

        if $attacker > $defender 
        {
            my $damage = ( 1 .. $.damage ).pick;
            say "The {self.WHAT.perl} inflicts {$damage} damage";

            $target.hitpoints -= $damage ;
        }

        if $target.hitpoints < 0
        {
            say "The {$target.WHAT.perl} is dead";
            $target.stopped = True;
        }
        else
        {
            say "The {$target.WHAT.perl} has { $target.hitpoints } hitpoints left";
        }
    }
}

class Player is GameObject does UnnecessaryViolence
{
    has $.name;

    multi method start
    {
        say "A hero was born.";
        self.start({
            # say "The hero is moving";
            # keyboard logic here, in the meantime random movement
            $.game.channel.send( { object => self, x => (-1 .. 1).pick, y => (-1 .. 1).pick } );
        });
    }
}

class Monster is GameObject does UnnecessaryViolence
{
    has $.species;

    multi method start
    {
        say "A monster hatched.";
        self.start({
            # say "The monster {self.id} is moving";
            # AI logic here, in the meantime random movement
            $.game.channel.send( { object => self, x => (-1 .. 1).pick, y => (-1 .. 1).pick } );
        });
    }
}

class Game
{
    my $idc = 0;

    has GameObject @.objects is rw;
    has Channel $.channel = .new;

    method run{
        self.setup;
        self.mainloop;
    }

    method setup
    {
        self.add-object( Monster.new( :id(++$idc), :species("Troll"), :hitpoints(20), :damage(14), :offense(3), :speed(300), :defense(3), :x(3), :y(2), :game(self) ) );
        self.add-object( Monster.new( :id(++$idc), :species("Troll"), :hitpoints(10), :damage(16), :offense(3), :speed(400), :defense(3), :x(3), :y(2), :game(self) ) );
        self.add-object( Player.new( :id(++$idc), :name("Holli"), :hitpoints(50), :damage(60), :offense(3), :speed(200) :defense(2), :x(0), :y(0), :game(self) ) );
    }

    method add-object( GameObject $object )
    {
        @!objects.push( $object );
        $object.start;
    }

    method remove-object( GameObject $object )
    {
        @!objects = @!objects.grep({ !($_ === $object) });
    }

    method mainloop 
    { 
        react {
            whenever $.channel.Supply -> $event
            {
                self.stop-game
                    if self.all-objects-stopped;

                self.process-movement( $event );

                self.stop-objects
                  if self.game-is-over;

            };
            whenever Supply.interval(1) {
                self.render;
            }
        }

    }

    method process-movement( $event )
    {
        #say "The {$event<object>.WHAT.perl} moves.";
        given $event<object>
        {
            my $to-x = .x + $event<x>;
            my $to-y = .y + $event<y>;

            for @!objects -> $object
            {
                # we don't care abour ourselves
                next 
                    if $_ === $object;

                # see if anything is where we want to be
                if ( $to-x == $object.x && $to-y == $object.y )
                {
                    # can't move, blocked by friendly
                    return 
                        if $object.WHAT eqv .WHAT;

                    # we found a monster
                    .attack( $object );
                    last;
                }
            }

            # -5 -1 5 
            # we won the fight or the place is empty
            # so let's move
            .x = $to-x ==  5  ?? -4 !!
                 $to-x == -5  ?? 4  !!
                 $to-x;

            .y = $to-y ==  5  ?? -4 !!
                 $to-y == -5  ?? 4  !!
                 $to-y;

        }
    }

    method render
    {
        for @!objects -> $object {
            "The {$object.WHAT.perl} is at {$object.x},{$object.y}".say;
        }
    }

    method stop-objects
    {
        say "Stopping";
        for @!objects -> $object {
            $object.stopped = True;
        }
    }

    method stop-game {
        "Game over. The {@!objects[0].WHAT.perl} has won".say;
        $.channel.close;
        done;
    }

    method game-is-over {
        return (@!objects.map({.WHAT})).unique.elems == 1;
    }

    method all-objects-stopped {
        (@!objects.grep({!.stopped})).elems == 0;
    }



}

Game.new.run;
like image 36
Holli Avatar answered Nov 05 '22 21:11

Holli


Supplies are asynchronous, not concurrent. You will need to use channels instead of supplies to feed them concurrently.

use v6;

my $i = 0;
my Channel $c .= new;
my $supply1 = start { for ^5 { await Promise.in(1); $c.send("B"); } };
my $supply2 = start { for ^5 { await Promise.in(0.5); $c.send("A"); } };

await $supply2;
await $supply1;
$c.close;

.say for $c.list;

In this case, the two threads start at the same time, and instead of using .emit, then .send to the channel. In your example, they are effectively blocked while they wait, since they are both running in the same thread. They only give control to the other supply after the promise is kept, so that they run apparently "in parallel" and as slow as the slower of them.

like image 3
jjmerelo Avatar answered Nov 05 '22 23:11

jjmerelo