Forking Perl with Select
From Beautifulcode
←Older revision | Newer revision→
architecture overview: deadlocks are avoided by using a robust queueing mechanism and ignoring redundant or late Go! shouts. See pre-box #3.
Why is this solution beautiful? With only two methods, there is no unnecessary cruft. The two methods could be implemented in any language, offering the contest participants their choice of implementation language without a big porting task to support the different languages. Any language that can to getc and putc will work.
For even easier contest participation, the socket to the participant could be appear as the stdin and stdout of an exec'd process instead of merely a launched library, with newlines bounding all the communications.
I've sort of done this before, with the Solitaire 500 challenge in the Perl Journal in 1996, but here you go
Invocation:
the program takes the names of the competing modules, each its own well-formed package file, as command line arguments.
For each of the modules, a bidirectional pipe is created, a fork is done, and then the module is loaded.
The tiles are shuffled.
Seven letters are written to each pipe's buffer, and a select loop is entered.
Referee to player mini-language: A-Z represent new letters.
Player to referee mini-language: "GO!\n" means I have completed my current puzzle.
Deadlocks are avoided by using non-blocking select and explicit buffers and only writing one character at a time. (which is silly, because the buffer is going to be bigger than 15 letters, but still) (the code below assumes that the pipe buffer can hold 15 chars, so doesn't bother with output buffers. ) (no it doesn't, it handwaves output buffering)
Verifying the correctness of player solutions is outside the scope of the problem.
Here's Speedscrabble.pl:
package main;
use Socket;
for $player (@ARGV){
my ($RefEnd, $PlayerEnd);
socketpair($RefEnd, $PlayerEnd, AF_UNIX, SOCK_STREAM, PF_UNSPEC);
if(fork){
# parent, stay ref
close $PlayerEnd;
push @Players, $RefEnd;
STUB make $RefEnd a "hot pipe" with non-buffered writes
}else{
# child, become player
close $RefEnd;
require $player;
$player->START;
die "$$: player $player returned from START";
};
};
@LetterPool = qw / W H A T E V E R
A L L T H E S C R A B B L E
T I L E S G O H E R E
/;
# distribute initial tiles
for (1 .. 7 ) {
for my $P (@Players){
print $P, shift @LetterPool
};
};
for (;;){ # endless loop
STUB compose selectmask from @Players (see Http::Server::Singlethreaded for working code)
output buffers are an array indexed by file descriptor number
players with data in their output buffers, we care if they are writable
we care if everyone is readable
STUB call select function
if (any players are readable AND have nothing in their outputbuffers){
read 4 chars from the player, should be "GO!\n"
pop from LetterPool into everyone's output buffer
};
if any other players are readable, read 4 chars from them as well
all players with data in their output buffer get a character if they are writable
};
package IPlayScrabble;
=pod
provide the LetterIfLetter and ShoutGO functions
provide @WordList word array
=cut
BEGIN {
chomp @WordList = `cat /usr/share/dict/words`;
};
sub LetterIfLetter(){
STUB "check $PlayerEnd for readibility or return false";
STUB "read one char from $PlayerEnd and return it";
}
sub ShoutGO(){
IsReadable($PlayerEnd) and return; # in case someone else shouted Go!
print "$main::player shouting GO\n";
print $PlayerEnd "GO!\n";
1 while !IsReadable( $PlayerEnd); # guarantee that LIL won't miss a letter
};
and here's a really poorly behaved (but slow) player called ScrabbleIdiot.pm
package ScrabbleIdiot;
@ISA = qw/IPlayScrabble/; # OO because that's what the spec says
bless \(my $me);
my @MyLetters;
push @MyLetters, $me->LetterIfLetter for 1..7; # take initial seven tiles
use Time::HiRes 'sleep';
sub START{
for (8..15){
sleep( 1 + rand 3);
print "$$: [@MyLetters] is a word, isn't it?\n";
my $nextletter = $me->LetterIfLetter;
if (!$nextletter){
$me->ShoutGO(); # for referee solution checking, ShoutGO(join '',@MyLetters);
$nextletter = $me->LetterIfLetter;
};
push @MyLetters, $nextletter;
# scramble provided letters randomly (because we're an Idiot)
for my $i (0..$#MyLetters){
my $r = int rand @MyLetters;
@MyLetters[$i,$r] = @MyLetters[$r,$i];
};
};
print "$$: Scrabble Idiot wins, with [@MyLetters]! please check my solutions\n";
};
1;
you could invoke like so:
perl Speedscrabble.pl ScrabbleIdiot
So. Two methods available to the player, LetterIfLetter and ShoutGO, and a well-defined
place to find the wordlist. A convention
for output to STDOUT for status. The big deadlock possibility, which is two players
simultanously shouting GO!, is taken care of by ignoring the second one: when there
is a GO! from a player who has a referee-queued letter (has not called LetterIfLetter
yet to receive it) that GO! is ignored.
proving it's correct. The possible tricky situations are when two players shout Go! at
the same time. I assume that the OS instantly updates pipes for readability as soon
as there is something written to one.
if we really must go threaded, we could have a shared flag indicating "someone has shouted Go!" and a mutex in the ShoutGo function. Forked and selecting, we can just follow the following table:
name of situation someone else shouted go! too what happens
happy path no everybody gets a tile
tie yes everybody gets a tile
late not only that but we've already we get ignored
been issued a tile that we haven't
picked up
