Sterling has too many projects

Blogging about Raku programming, microcontrollers & electronics, 3D printing, and whatever else...

»

  • Category: Blog

I am going to be honest and upfront with you. I have almost no opinion about the name change of Perl 6 to Raku. I can see going to war for something meaningful, like religion. I cannot imagine doing so for something trivial, like a programming language. I won’t be upset if you call my hammer a mallet. There are certainly advantages and disadvantages to the name change, but as far as I’m concerned, it was inevitable as long as the issue kept being brought up year after year.

However, I do want to take this opportunity to tell a story about my history with programming, the Perl language, and how my story intertwines with Raku.

Commence third person:

Once upon a time, there was a nerdy little boy named Andy. Andy’s father was nerdy too, in his own way. His father had a thing for electronics. He spent his daytime hours helping people from his ambulance, then helping people buy and sell homes. After that he was helping people learn the benefits of Augmentin, Timentin, Paxil, Bactroban, and whatever other pharmacological wonders Beecham or SmithKline-Beecham were making. (His son remembers the names of all these because he still has a few packs of pens or notepads with these names on them sitting around multiple decades and half a dozen moves later.) Despite his day time work paying the bills, he maintained a certain evening passion for computers and electronics.

Andy’s father thought personal computers were really cool. He would visit the hobby computer store downtown and look over all the 'wares. He would browse computer magazines like Compute! and checkout the latest and greatest equipment and software ads. He didn’t have a lot of money, so he decided to start modestly, with an Atari 400. Armed with an Atari 400, Atari BASIC, and a special cassette tape deck, he taught Andy how to copy BASIC programs out of Compute! magazine to make simple video games. A new software developer was conceived in the slow hours of pushing buttons on that horrid membrane keyboard. Being only 5 or 6 and barely able to read, his programs didn’t work, but his father painstakingly debugged these programs, armed only with a pre-med degree, a desire to teach, and a love for these quirky electronic computer boxes.

As Andy got older, Andy got tired of being Andy. He decided that he need a more mature moniker. So he became Andrew. Andrew’s father eventually replaced the Atari 400 with a Zenith Heath Kit 8086 (properly pronounced the “eight-oh-eighty-six” in case you didn’t know). It was an amazingly heavy and bulky machine, with an external keyboard and a green monochrome monitor. It was very powerful, though, with its bank of 16KB of memory and two 5¼" double-density floppy drives. No more fiddling with cassettes! But it had to be assembled. It was a Heath Kit after all. While Andrew’s dad was assembling this 8086, solder by solder, Andrew spent the summer taking computer classes from Ms. Francisco at the local community center.

One of Andrew’s father’s favorite stories is the day he finished putting that 8086 together. He snarkily said to his son, “Well, now that it’s built, what do we do with it?” His son, being a very literal young man rolled his eyes and replied, “You put a disk in like this and turn it on like this.” He quickly booted up this new machine and showed his father how to use this new fangled DOS thing.

Andrew continued to copy programs out of new editions of Compute!, but now for GW-BASIC running on Zenith Z-DOS 1.19. He also began dabbling in writing his own programs. These were extremely simple, but his knowledge was beginning to expand and mature. Along the way the 8086 picked up another 48KB of RAM, a 20MB hard drive, a text-to-speech board, a 2400-baud modem, and then a final upgrade to 640KB. That final upgrade mod involved Andrew’s father spending days soldering hundreds of memory chips to a circuit board that seemed a mile long. He had to hot wire in extra connections from the power supply to allow it to work as the regular bus connection could not provide the required amperage by itself.

Around the age of eleven, things were changing rapidly for Andrew. The old 8086 was not aging well and was replaced by an 80386DX. His best friend, Lucas, who would eventually secure his own pre-med degree, had recently secured a copy of Turbo Pascal. He reluctantly shared a bootleg copy of this software as well as the name of a file on the local Sherwood dial-in BBS containing a Pascal tutorial. Armed with a ream of continuous feed paper printed all day and all night on the tremendously loud Epson dot-matrix printer, he began to teach himself the wonders of Pascal.

After moving from his childhood home in Lawrence, Kansas to a new home in Wichita, Andrew kept working in Pascal. He had few friends in his new home and none with whom he really felt close. In a period which is as pathetic as it sounds, Andrew’s computer was his best friend, now a 486DX2/66. Continuing in Pascal, he could build tools for generating character sheets and manage the Mechwarrior games and other table top games he played with his brother. (Though, we called them “RPGs” even when they weren’t RPGs in those days.) Soon, Pascal started to even edge out video games as a dominant use of his time.

High school offered new opportunities to Andrew. The school taught an intro to programming course and an AP computer science course in Pascal. Pascal was old hat. He thought it would be a breeze and it was. He completed the intro course on the ancient Apple 2e computers the school still had in only 4 weeks, and spent the rest of the semester helping others and writing graphics demos for fun. The AP course was completed with equal speed, but it taught him several new tricks he hadn’t learned before along the way but now on newly acquired Apple PowerMacs.

At this point, however, he was no longer satisfied with Pascal. He really wanted to learn the language that all the best software of the day was written in, C++. [insert laugh track] He managed to get his hands on a copy of Turbo C++ and delved into the details of memory models, embedded assembly, and graphics drivers. For his sixteenth birthday, Andrew’s father did not buy him a car or any of the things more typical 16 year old might like. Instead, Andrew received Borland C++ 3.1 with Application Framework. Twenty pounds of manuals, forty 3½" floppy installation disks, and all. Best. Birthday Present. Ever.

Along the way here, he became convinced that Christianity was a pretty big deal in his life. And so, when he went to choose a college, he picked one where he could learn more about Christianity and computer science. At the small college where he ended up in, there were two other Andrews on his floor. They decided that it was too confusing for everyone to be Andrew, so the sophomore kept his name and the two freshman changed to use a different name. Our Andrew became Sterling, a middle name Andrew shared with his grandfather, who despised the name. (His grandfather much preferring his own first name, Delmont, but that’s a story for another time).

At college, everything was Java because Sun had managed to convince State U. to teach it. Sterling definitely never liked Java. It always seemed slow, verbose, and weirdly stilted. He was used to passing around function pointers, Java had none. Sterling had become accustomed to releasing his own resources, Java didn’t let you, preferring instead to lock up your program for multiple seconds at a time to do that for you more efficiently. He much preferred C++, but even C++ was troublesome, especially as that language continuously increased in complexity in a false search for perfection. He wasn’t satisfied with what he knew. He even began reading books on language design and grammars with the thought that maybe he wanted something different.

However, life goes on and while he may have written a few toy langauges, he didn’t dive deeply enough to make anything meaningful. Besides, he needed a real job. So to help pay for college (and pay for being married and to help his wife through college), he took a job as a Network Consultant. Between classes he would travel around northeast Kansas in his Dodge Dakota and plug in computers at grain distributors, configure NetWare servers at accounting offices, and pull all-nighters to rebuild Linux mail servers for police stations, etc.

Along the way, Sterling encountered his first Perl program, Anomy Sanitizer, an early e-mail protection program. Sterling’s boss wanted to customize it to deal with a particular problem one of his clients was having. Sterling did and in the process was introduced to Perl 5.6. He didn’t think much about it in 1999. Perl looked sufficiently C-ish that he could wade through it without really learning it. So he solved the problem and moved on.

As he finished up his undergraduate degree and begin grad school (to the surprise of everyone, including himself), he briefly took a job as a research assistant. The research Sterling was helping with generated copious piles of data in long log files. However, to understand the data, he needed to summarize it, as anyone familiar with C can tell you, parsing textual data in C and summarizing it is a real pain, which is why almost no one uses it for that task directly. Sterling decided to write the summarizer in new language he’d just heard of called Python and quickly discovered he was not a fan. So he decided to try again in Perl, and he found the language to be familiar and expressive and wondered why he hadn’t noticed how nice it was during his previous experience.

This was around the time when Perl 5 was first experiencing some pain around major changes certain members of the Perl community wanted to see made to the language. This was the era of the first Perl Apocalypses. As Larry and Damian began writing apocalypses, then exegeses, and finally synopses, Sterling read these avidly and found them fascinating. Sterling drank it all up. He really liked the way Larry Wall thought about software problems. Part of it was the way he entangled his Christian beliefs was intriguing. This just felt right to Sterling. So Sterling decided that if he could, he wanted to work in Perl until this Perl 6 become a reality and he could work in this fantastical new language.

Over the years, he watched developments of tooling like Parrot and Pugs and MoarVM and NQP, mostly from afar. Though, Sterling did briefly teach a Computer Architecture class at State U. He taught the VM part of the class using Parrot back in 2003. He also completed his Master’s Report using Perl to build a multi-agent system coordinated through a Perl-based ticketing system, called RT.

Then, he got a real job. His real jobs initially involved programming PHP, Perl, and Java, then focused on Perl and JavaScript, and later became an amalgam of Perl, Python, JavaScript, Go, Bash, and various others. His hobby and home projects generally involved Perl, but also Bash, C, C++, Java, JavaScript, PHP, Python, and Scala. As with anyone who lives in the unix-ish world, Sterling was a polyglot, of course.

Fast forward to around 2015 and the rumors were getting louder that when Larry Wall said Perl 6 would be released on Christmas, he meant this Christmas. It was finally time to start really digging in. Sterling started by seeing about porting some modules. He then got to remake his efforts following the Great List Refactor of 2015. Since 2015, Sterling has worked to steadily improve his fluency in the language, releasing more than a dozen modules to the Perl 6 ecosystem, and giving some talks at The Perl Conference related to his O(fun) work.

For those fun at-home projects in his spare time, if it can be reasonably done in Perl 6, Sterling has been doing it in Perl 6. Moving forward, if it can be done in Raku, he will be doing it in Raku. However, over the past 20 years, he’s also developed a deep and loving knowledge of Perl 5. Perl 5’s greatest weakness is its commitment to reverse compatibility. Raku’s greatest weakness is its commitment to break things in a significant way every year. This year’s break: Renaming to Raku.

That’s Sterling’s story of Perl/Raku, the Universe, and Everything.

Returning to first person, Perl still pays my bills (rather well!) and I see no reason to believe that’s going to change. As of this writing, I just completed my first production project in Raku as a volunteer gig. (I hope to write about that soon.) Yet, I have never been paid to write Raku and can’t imagine at this point when I ever will be. Times change, but I’m a go-with-the-flow kind a guy, so it’ll happen if and when it happens. In the meantime, I’m having fun using both of these hammers (or mallets) to bang on the problems I come across. I love both Perl and Raku and I don’t intend to give up loving either of them just because of this name change. I hope to keep a foot in both communities as well.

Cheers.

P.S., Special thanks to my wife who helped edit this. I love you.

»

  • Category: Blog

First, I want to say thank you to Andy Lester who has been project lead on vim-perl6 as well as the other contributors, especially Hinrik Örn Sigurðsson, Rob Hoelz, and Patrick Spek.

The plugin will now be homed in the new Raku organization on github here:

https://github.com/Raku/vim-raku

As of right now, it has been updated such that it will handle the old Perl 6 filenames as well as the newer ones, so far identified as .raku, .rakutest, and .rakudoc. It also handles .t6 which is something that the plugin didn’t handle previously.

I also renamed the internals so most of the references to p6 or perl6 have been changed to raku, so when you update the plugin, if you made use of any of the (mostly undocumented) perl6_ variables you will need to update those settings accordingly. I have not made any non-trivial changes to how the plugin is structured or works.

I have also modernized several of the keyword lists. For example, the async keywords such as react, supply, and whenever will now highight as well as several previously unhighlighted traits such as DEPRECATED and pure. I have also removed highlighting for some older keywords that date back to Pugs, such as async and defer.

I will try to work with the Bram Moolenaar to get these changes into the vim release, if I can. However, in the meantime, you’ll probably need to install the module from the master branch if you want it.

I will begin migrating issues from the old vim-perl6 project and see if I can take a whack at any of them that are outstanding. However, I would very much appreciate help with maintenance on this, so if you depend on vim and make use of this plugin, I would very much welcome your feedback and pull requests.

Happy New Year and Cheers!

»

I hope this calendar has been of some use to you all. In any case:

Merry Christmas!

»

Lots of big words in the title. In simpler terms, it means running a program in the background and interacting with it as input and output becomes available. The tool in Raku for doing this work is called Proc::Async. If you’ve ever dealt with the pain of trying to safely communicate with an external process, writing to its input and reading from its output and error streams and hated it, I think you’ll like what Raku has built-in.

First, let’s contrive a problem. Let’s make an external program that takes strings as input and reverses them and outputs them on the standard output. Meanwhile, it reports on standard error whether or not the given string is a palindrome or not. We could write this program like so:

for $*IN.lines -> $line {
    my $rline = $line.flip;
    say $rline;
    note ($rline eq $line);
}

And just to make it clear, for this sample input:

spam
slap
tacocat

We will get this output:

maps
False
pals
False
tacocat
True

The True and False lines being standard error and the other lines being on standard output. Clear? Good.

Next, to finish our contrived problem, we need to interact with this program and just write out a message like tacocat is a palindrome! because it is exciting whenever we see a palindrome. We want to output nothing otherwise. Let’s use Proc::Async to interact with our other program, we’ve called palindromer for giggles.

react {
    my $palindromes = Supplier.new;
    my $p = Proc::Async.new: './palindromer', :w;
    my @rlines;
    my @palindrome-checks;

    # Echo our own input
    whenever $*IN.Supply.lines -> $line {
        $p.say: $line;

        # Let palindromer know when we run out of input
        LAST { $p.close-stdin }
    }

    # Watch for the reverse lines on palindromer's standard output
    whenever $p.stdout.lines -> $rline {
        if @palindrome-checks.shift -> $is-palindrome {
            $palindromes.emit: $rline;
        }
        else {
            push @rlines, $rline
        }
    }

    # Watch for the True/False output from palindromer's standard error
    whenever $p.stderr.lines -> $is-palindrome {
        if @rlines.shift -> $rline {
            $palindromes.emit: $rline if $is-palindrome eq 'True';
        }
        else {
            push @palindrome-checks, $is-palindrome;
        }
    }

    # PALINDROMES ARE EXCITING!
    whenever $palindromes.Supply -> $palindrome {
        say "$palindrome is a palindrome!";
    }

    # Quit when palindromer quits
    whenever $p.start { done }

}

Now, if we pipe the same input as before into our new program, we should get this output:

tacocat is a palindrome!

Our code deals with the potential problem of standard output and standard error getting out of sync by using a queue to accumulate extra values from each side. We feed the discovered palindromes into a central Supplier named $palindromes so we can have a single place for printing the palindromes we find.

A couple key points to be aware of when using Proc::Async:

  1. Always put the .start call after your tap on standard output and standard error. Otherwise, there could be problems with missed lines (i.e., lines emitted before you are listening). Raku will warn you if it detects that you have done this, by the way. This method returns a Promise that is kept when the program is finished.
  2. Make sure you use .close-stdin when you are finished with input to make sure the other program knows that you’re done.

Otherwise, there are a few more interesting features, such as .ready amongst others, which you might want to read more about in the Raku reference documentation online.

Cheers.

»

Raku actually provides two different locking classes. A Lock object provides a very standard locking mechanism. When .lock and .unlock are used or .protect is called, you get a section of code that pauses until the lock frees up, runs while holding the lock, and then frees the lock so other code that might be waiting on the lock can run.

However, the Lock class works in such a way that blocks the current thread. As I’ve pointed out earlier in this advent calendar, the purpose of threads is to do stuff, so blocking them from running is preventing them from fulfilling their purpose. Luckily, there is a solution.

In cases where you want locking, but don’t want to burn your threads waiting for the lock to free up, consider using Lock::Async instead of Lock. It works very similarly to Lock, but the .lock method does not block. Instead, it returns a Promise which will be kept when the lock is free. Code that awaits that Promise will pause in a way that allows Raku to reuse the current thread for another task:

class SafeQueue {
    has @!queue;
    has Lock::Async $!lock .= new;

    method enqueue($value) {
        await $!lock.lock;
        push @!queue, $value;
        $!lock.unlock;
    }

    method dequeue(--> Any) {
        $!lock.protect: { shift @!queue }
    }
}

The code above demonstrates both the use of .lock and .unlock as well as .protect. You should always prefer .protect as the code in enqueue above might leave the lock forever held if an exception gets thrown after the lock is acquired. From the point of view of your program, a .protect will behave similarly between Lock and Lock::Async, but internally performs an await on the .lock method. This means that the thread the code is running on will be freed to be used by another task that is waiting to be scheduled.

Cheers.

»

What’s more asynchronous than socket communication? When two programs need to talk to each other, often from different computers on different networks in different parts of the world, you can connect using a socket. Whether an HTTP server or some custom protocol, you can implement both sides of that communication using IO::Socket::Async.

Let’s consider a simple calculator service. It listens for connections over TCP. When a connection is established, it takes lines of input over the connection and parses each line as a simple mathematic calculation like 2 + 2 or 6 * 7.

We can write the server like this.

react {
  whenever IO::Socket::Async.listen('127.0.0.1', 3456) -> $conn {
    whenever $conn.Supply.lines -> $line {
      if $line ~~ m:s/$<a> = [ \d+ ] $<op> = [ '+' | '-' | '*' | '/' ] $<b> = [ \d+ ]/ {
        my $a = +$<a>;
        my $b = +$<b>;

        my $r = do given "$<op>".trim {
          when '+' { $a + $b }
          when '-' { $a - $b }
          when '*' { $a * $b }
          when '/' { $a div $b }
          default { "Unknown Error"; }
        }

        $conn.print("$r\n");
      }
      else {
        $conn.print("Syntax Error\n");
      }
    }
  }
}

Now, the nested whenever block might look a little odd, but this is just fine. You can add more whenever blocks within a react any time you need to this way.

The outer whenever listens for new new connection objects. It’s sole job here is to register the connection as another whenever block with the server. Be aware that using this strategy does mean that you are handling all connections asynchronously as if from a single thread. A more scalable solution might be to use a start block for each arriving connection (which might look something like this):

start react whenever $conn.Supply.lines -> $line { ... }

Moving on, the inner whenever then watches for lines of input from each connection as it arrives. It will receive a message whenever a line of input has been sent by the client. This code parses that line, performs the expression (or discovers an error), and returns the result.

Simple.

We call listen to establish a listening TCP socket on the named address and port number. This returns a Supply which will emit connected IO::Socket::Async objects. You can use the Supply method on the connection object to get text (or bytes via the :bin option), whenever the are sent from the associated client. You use the write and print methods to send bytes and text back.

The client can also be written with IO::Socket::Async as well. Here is a client which uses our expression server to calculate the Fibonacci sequence:

my ($a, $b) = (0, 1);
say "$a";
say "$b";
await IO::Socket::Async.connect('127.0.0.1', 3456).then: -> $connection {
  given $connection.result -> $conn {
    $conn.print("$a + $b\n");

    react {
      whenever $conn.Supply.lines -> $line {
        $a = $b;
        $b = +$line;
        say "$b";
        $conn.print("$a + $b\n");
      }
    }
  }
}

When making a client conneciton, we use the connect method with IP address or host name of the server to connect to. That method returns a Promise which is kept once the connection has been made. The result of that promise is a connected IO::Socket::Async object which can be used in precisely the same way as the server, with Supply returning text or bytes and write and print being used to send text or bytes.

Cheers.

»

Iteration is slow. If you have N things to process in a loop, your loop will take N iterations to process. Slow. Sometimes that’s the only way, though, to solve a problem.

For example, let’s consider the case where we have a JSON log and we want a command to read each line, parse the JSON for that log, and summarize it showing the time stamp and message:

use JSON::Fast;
my $log-file = 'myapp.log'.IO;
for $log-file.lines -> $line {
  my %data = from-json($line);
  say "%data<timestamp> %data<message>";
}

If you have multiple cores on your system (and who doesn’t in 2019?), you can actually speed this up a little bit with a small change:

use JSON::Fast;
my $log-file = 'myapp.log'.IO;
race for $log-file.lines -> $line {
  my %data = from-json($line);
  say "%data<timestamp> %data<message>";
}

The race prefix added to any loop will result in the items being iterated as quickly as possible on the available cores. On my machine, for a short 10,000 line log with only these two fields in it results in about a 25% time savings. However, this comes with a consequence: the original order of the lines is no longer preserved. In some cases, this might not matter, but in others it does matter.

Now, there is another prefix we could use that preserves order, called hyper. However, in this particular case, it won’t work. Why? Because hyper only guarantees the results will be output in order, but here we are outputting the results as the code is run. This is something to be very careful of whenever working with these keywords.

However, this is easy to fix. You just need to eliminate the side-effects and make your for loop functional:

use JSON::Fast;
my $log-file = 'myapp.log'.IO;
my $output-lines = hyper for $log-file.lines -> $line {
  my %data = from-json($line);
  "%data<timestamp> %data<message>";
}
.say for @$output-lines;

Now, we get most of the speedup from parallelizing the parsing of JSON lines, but we can output in the same order as the original file. This works because the output of a for loop with a hyper or race prefix works just like do: the result is a sequence that we can iterate. In this case, it’s a HyperSeq which makes sure Raku handles the multi-threading bits correctly.

Cheers.

»

The goal of today’s article is to consider when you want to run your tasks simultaneously and how to do that. I am not going to give any rules for this because what works one time may not work the next. Instead, I will focus on sharing some guidelines that I have learned from personal experience.

Remember Your Promises

Whenever you use concurrency, you want to hold on to the related Promise objects. They are almost always the best way to rejoin your tasks, to cause the main thread to await completion of your concurrent tasks, etc.

A common pattern I see in my code looks like this:

my $gui-task = start { ... }
my $console  = start { ... }
my $jobs     = start { ... }
await Promise.allof($gui-task, $console, $jobs);

Just like that, I have three tasks running in three different threads and the main thread is holding until the three tasks complete.

That await is also where you want to add your CATCH blocks as that’s the point at which exceptions from the other threads will rejoin the calling thread.

The Main Thread is Special

When you write your concurrent program, be aware that the main thread is special. It will not be scheduled to run a task and your program will continue to run as long as it is doing something or awaiting on something. As soon as the main thread exits, your other tasks will immediately be reaped and quit.

Prefer a Single Thread for Input or Output

Avoid sharing file handles or sockets between threads. Only a single thread can read or write to a single handle at a time. The easiest way to make sure you do that safely is to keep that activity in a single thread. On a multi-threaded program where any thread may output to standard output or standard error, I often invoke a pattern like the following:

my Supplier $out .= new;
my Supplier $err .= new;
start {
    react {
        whenever $out { .say }
        whenever $err { .note }
    }
}
start { 
    for ^10_000 { $out.emit: $_ }
}
start {
    for ^10_000 { $out.emit: $_ }
}

If you don’t employ a pattern like that, your program will probably still work, but you may end up with some strange oddities with your output.

Raku Data Structures are Not Inherently Safe

Similar to what was said in the previous section for input and output, please note that most Raku data structures are not thread safe. If you want to use a data structure across threads, you must use some strategy for making that access thread safe. Some strategies that will work are:

  • Use a thread that manages access to that data structure, as was done above for standard output and standard error.

  • Use a monitor pattern to secure the data structure.

  • Make use of cas, Lock, Lock::Async, Semaphore, Promise, or one of the other locking mechanisms available to guard access to the object as appropriate.

  • Manage the modifications to the object using a Channel or Supply.

Whatever you do, do not assume access to an object is thread safe unless thread safety is explicitly part of the design of the object.

Use a Task per GUI Event Loop or Window

If your application has a GUI, you almost certainly want a separate thread for managing input and output with the GUI. Most GUI libraries have a built in event loop already and you want to run that as a task in a separate loop. You may want a single task for your whole GUI or you may want a separate task per Window.

Batch Small Tasks

You do not always want to have a single task for every action. Some actions are simply too trivial and the execution is too short to manage this. What is a reasonable size of task is really up to you and your execution environment. Just be aware that running your tasks in batches is often a better strategy than running them in tiny bits when the processing involved is trivial.

If you use the hyper or race keywords or methods to parallelize work, batching is built-in and automatic. You may want to experiment with the parameters to see if tuning the batch sizes of your task results in speed increases.

Break Larger Tasks into Smaller Ones

Some concurrent tasks you just want to run continuously as CPU time is available or trigger whenever an event comes available. However, single run tasks that run long can sometimes benefit from being broken down into smaller ones. There are only a finite number of tasks that can run simultaneously and breaking them down can help make sure that the CPU stays busy.

One easy way to break down your tasks is into insert await statements in natural places. As of Raku v6.d, you can effectively turn your tasks into coroutines by pausing with an await until a socket is ready, more data comes in, a signal arrives from a Promise or Channel, etc. Remember that any time Raku encounters an await, it is an opportunity for Raku to schedule work for another task on the thread the current task is using.

Beware of Thread Limitations

There are a limited number of threads available. If you have a task with the potential to run a large number of tasks, take some time to consider how the tasks are broken down. Limiting the number of dependencies between tasks will allow your program to scale efficiently without exhausting resources.

Any time your tasks must pause for input or for whatever reason, making sure to do that with an await will ensure that the maximum number of threads are ready for work.

Avoid Sleep

I consider sleep to be harmful. Instead, prefer await Promise.in(...) as that gives Raku the ability to reuse the current thread for another task. Only use sleep when you deliberately want to lock up a thread during the pause. I make use of sleep in this Advent calendar mostly because it is more familiar. In practice, I generally only use it on the main thread.

Conclusion

Much of this advice overlaps the advice on breaking up async problems. I hope this provides some useful guidelines when writing concurrent programs.

Cheers.

»

As I have said several times before in this calendar, it is always best to avoid sharing state between running threads. Again, however, here is yet another way to share state, when you need to do it.

A few days ago, we considered monitors as a mechanism for creating a thread-safe object. Let’s consider the following monitor:

class BankBalanceMonitor {
    has UInt $.balance = 1000;
    has Lock $!lock .= new;

    method deposit(UInt:D $amount) {
        $!lock.protect: { $!balance += $amount };
    }

    method withdraw(UInt:D $amount) {
        $!lock.protect: { $!balance -= $amount };
    }
}

The day after that we considered the compare-and-swap operation, a.k.a. cas, and how to use it with any scalar variable in Raku. By using cas, we can actually create thread safe objects without using locks at all.

Thus, we can rewrite the above class as a lock-free data structure like this:

class BankBalanceLockFree {
    has UInt $.balance = 1000;

    method deposit(UInt:D $amount) {
        cas $!balance, -> $current { $current + $amount };
    }

    method withdraw(UInt:D $amount) {
        cas $!balance, -> $current { $current - $amount };
    }
}

That’s it. Same protections, but now we’ve made use of the scalar CAS operation instead. This can be more efficiently than locking. But why?

Locks have a cost at the beginning and end every time the lock is encountered. Add to this the fact that the every critical section is a bottle neck where a multi-threaded system must become single-threaded for a moment. Whereas CAS has no particularly expensive operations, but might cause the critical section to re-run multiple times.

Let’s consider the extremes of two variables in our system: contention and run time. Contention is a generic term describing the number of threads needing to work in the critical section at once. Run time here describes how long it takes to run the operation inside the critical section.

If an operation has low contention and short run time, CAS is almost certain to perform better. Locks have high overhead at start and end, whereas CAS is going to have almost no overhead. With low contention we might have to repeat an operation every now and then, but the operation is fast, so it doesn’t matter.

If an operation has high contention and short run time, CAS is still likely to win. You could end up with a thread or two having to repeat the operation several times, but with a larger number of threads a lock’s enforcement of a single-threaded bottleneck does not scale well.

If an operation has low contention and long run time, CAS might be a loser. If the critical section really must take hundreds of milliseconds or even longer, repeats are likely to be more costly. It may be worth A/B testing to see which wins, though.

If an operation has high contention and long run time, locks may win. At this point, however, it becomes less clear if your operation is really scalable across multiple threads at all. The bottleneck of locking for a long time on many competing threads essentially reduces your application to single-threaded. It might be time to consider how you can speed up the operation or do it in a way that doesn’t involve shared state.

Cheers.

»

In Raku, a Supply is one of the primary tools for sending messages between threads. From the way a Supply is structured, it is obvious that is provides a means for one ore more tasks to send events to multiple recipient tasks. What is less obvious, however, is that a Supply imposes a cost on the sender.

Consider this program:

my $counter = Supplier.new;
start react whenever $counter.Supply {
    say "A pre-whenever $_";
    sleep rand;
    say "A post-whenever $_";
}
start react whenever $counter.Supply {
    say "B pre-whenever $_";
    sleep rand;
    say "B post-whenever $_";
}
start for 1...* {
    say "pre-emit $_";
    $counter.emit($_);
    say "post-emit $_";
}
sleep 10;

Here we have three tasks running, each in a separate thread. We let the main program quit after 10 seconds. The first threads two receive messages from the $counter.Supply. The third thread feeds a sequence of integers to this supply. You might be tempted to think that the final task will race through the delivery of events, but if so, you’d be wrong.

Consider the output of this program:

pre-emit 1
A pre-whenever 1
A post-whenever 1
B pre-whenever 1
B post-whenever 1
post-emit 1
pre-emit 2
A pre-whenever 2
A post-whenever 2
B pre-whenever 2
B post-whenever 2
post-emit 2

Notice a pattern? Even though there’s a random wait in the first two threads and no wait at all in the third, the third thread is blocked until both of the other threads complete. This behavior is the same regardless of how the Supply is tapped, i.e., it does not matter if you use whenever blocks or call .tap.

Therefore, if you want your emitter to blast through events as quickly as possible, you need to make sure the taps are written to finish as soon as possible or consider a different solution, such as using a Channel which will queue tasks in memory and they will get processed whenever the thread listening to that channel has time to process them.

Just be aware of this back pressure cost whenever using a Supply. The sender always pays.

Cheers.