Concurrency and Channels in Perl 6

I've been working on a Bayesian spam filter, but it keeps running out of memory, so I moved to something else for a while. The new concurrency stuff looks really interesting, but I don't understand it well yet. As a project, I came up with the idea of a password cracker, which would check a crypt-style hash against a word list. (This probably isn't a CPU-intensive enough task to be worth threading, but it was simple.) Here's the code, with details below:

#!/usr/bin/env perl6
use v6;
use Crypt::Libcrypt;

sub MAIN( $encrypted, $wordfile, $units=5 ){
    my $salt = $encrypted.substr: 0, 2;
    my $stream = Channel.new;
    start {                                      # 1
        for $wordfile.IO.slurp.words -> $w {
            $stream.send($w);
        }
        $stream.close;
    };

    my $match;
    await do for ^$units -> $u {                 # 2
        say "Starting unit $u";
        start {
            loop {
                if $stream.closed.not and        # 3
                   $stream.receive -> $w {       
                    say "Trying $w in unit $u";
                    if crypt( $w, $salt ) eq $encrypted {
                        $match = $w;
                        $stream.close;
                    }
                    sleep 1;
                } else {
                    last;
                }
            }
        };
    };
    say "Found it!: $match" if $match;
}

I call it with: ./crackpass.p6 abPRdpgdfUoM. wordlist 5

That encrypted string will match the word "george" which is in the "wordlist" file.

1) Starts a thread which opens the word file and starts sending the words to the Channel $stream.

2) Starts 5 (or however many specified on the command line) threads. Each one loops, getting the next word in $stream and checking to see if it matches the encrypted string.

3) Checks to see if $stream is closed, then gets a word from it if it's not. There's a race condition here, because if I remove the sleep line, sometimes I get "Cannot receive a message on a closed channel". I think what's happening is one thread gets to the first half of the if statement, sees that the $stream isn't closed, then goes to the second half, but in the meantime another thread takes the last word from $stream and lets it be closed, so the first thread's $stream.receive errors. There must be a way to handle that, but I haven't figured it out yet. I know I could wrap it in a try/catch, but that seems like a kludge. Maybe I should be using earliest or something like that.

Except for that race condition, which doesn't always happen, it does work. I have no idea how much actual concurrency is going on on my system (FreeBSD on dual-core amd64), but it's fun anyway. I'm trying to think of something more interesting to do with these tools, that would really show off what they can do -- once I understand it myself.

2 Comments

Ohai!

One change you could make is to use $stream.poll, which will simply return Nil if no item could be grabbed. In response to getting Nil, you can then check $stream.closed to decide whether to try again or terminate the unit.

However, if the thread that consumes the wordlist and fills the channel somehow gets stuck (for example, if the file is on a remote filesystem, or if the hard drive starts spitting out errors or whatever), it'll end up busy-looping.

For extra robustness I'd suggest you put the "$stream.close" outside of the start { } block that reads the file, because if any exception happens there, the start block will immediately get exited.

my $file-reader = start { ... };
$file-reader.then({ $stream.close })

Inside the block for ".then" you could inspect the promise for kept/broken, so that you can print out any error that may have occured, too.

One other thing I saw in the documentation of Channel is that you can also await the channel, which will return a value that has been nommed. It does not say what happens when the awaited channel closes in the mean time, but it does say it uses receive, so it probably just throws the ReceiveOnClosed exception for your convenience.

In any case, handling an exception in this case doesn't seem like a kludge to me. Instead of building a big "try + catch", you can write something like

(try $stream.receive) orelse last

Hope that helps!
- Timo

Leave a comment

About Aaron Baugher

user-pic I'm a programmer and Unix sysadmin who uses Perl as much as possible, operating from the Midwest USA. To hire me for sysadmin or programming work, contact me at aaron.baugher @ gmail.com or as 'abaugher' on #perl6.