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

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.


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.


Thanks to jjmerelo here, I managed to get it working. The channel was the right track, but you actually have to consume the channels supply.

use v6;

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

react 
{ 
    whenever $c.Supply -> $x { $x.print };
}

$c.close;

Additional question: How good does that scale? Can you have several thousand supplies sending to the channel?


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;

Tags:

Raku

Rakudo