The base of PLT's thread primitives are documented in the PLT reference documentation. Specifically, to spawn a thread, we just need to call
(thread <procedure>)
. To send the thread a message, use (thread-send <thd> <msg>)
. The target thread should call (thread-receive)
to retrieve the message. This basic pattern works well with two threads communicating with each other. Once more than two threads are involved, we run into issues with just (thread-receive)
since we have no way to verify which thread sends which message, let along sending back the right response to the right receipient. We'll need something more.Introducing bzlib/thread - implementing the erlang selective receive pattern in PLT Scheme. The code is released under LGPL.
In Erlang, the
receive
is pattern match enabled, and we want something similar in PLT scheme. bzlib/thread
provides the pattern match enabled receive for PLT Scheme, with the syntax receive/match
:
(require (planet blib/thread)) ;; load the package.
;; default syntax - with pattern match
(receive/match
(match-pattern exp ...) ...)
;; timer syntax - just the timer.
(receive/match
(after time exp ...))
;; extended syntax - combine the pattern match with timer.
(receive/match
(match-pattern exp ...) ...
(after time exp ...))
The match-pattern has the exact same syntax as
scheme/match
, since scheme/match
provides the underlying matching capability. You can also specify an after
clause so you can specify the number of seconds (does not have to be integer) elapsed for a timer-based event to trigger even without thread messages.Beyond Erlang's Receive Capability
While PLT Scheme's concurrency capability is not yet as capable as Erlang, its language facility has more to offer than Erlang's. By default PLT Scheme provides a powerful synchronization framework that includes many different types of events (the
after
clause is built on top of the alarm event), and it would be a shame if we cannot take advantage of all those event capabilities within our receive/match
, hence receive/match
is enhanced with a sync
clause:
;; with the sync clause - it is also pattern-match enabled...
(receive/match
(pattern-match exp ...) ...
(after time exp ...)
(sync (pattern exp ...) ...))
Hence you can pass in a list of custom events so the syntax will handle all of the synchronizations within one clause. The
sync
clause also have pattern matching, so you can use it to match against the specific events that was triggered and dispatch to the correct clause branch.The
receive/match
form the basis of multi-threads communications.Communication Bewteen Multiple Threads
As stated earlier, the challenge with the bare
thread-send
and thread-receive
is that you have to handle your own message dispatching if there are multiple threads trying to communicate with each other. While receive/match
provides the additional pattern matching capability, it only simplifies your effort to coordinate all of the threads, but you still have to devise the scheme in which you can identify the sending thread (so you can send back an appropriate response). To do so, we need to codify the structure of the message so it includes information about the sender. The simplest way is to add the sending thread into the message as follows:
(thread-send thd (list (current-thread) args))
Then you just need to ensure your
receive/match
matches the signature:
(receive/match ((list (? thread? thd) args) (do-whatever) ...) ...)
bzlib/thread
codifies the above signature in thread-call
, so all you have to do is:
(thread-call thd args)
;; or with a timeout
(thread-call thd args timeout)
and the above message signature will be sent.
Once you've received the message via
receive/match
and extracted your args for processing, you can then use thread-reply
to send a message back to the sender:
(thread-reply sender result)
;; or you can reply on behalf of another thread
(thread-reply sender result thread-on-behalf-of)
Handling Exceptions and Respond to Sender
What if the processing raised an exception and you need to send the exception back to the sender? To avoid confusion between a regular reply (which holds legit values) and an exception, you should use
send-exn-to
to return the exception, which does the following:
(thread-send thd (cons exn thread) #f) ;; #f means no error is raised if the receiving thread is dead.
Then you just match for the exception pattern to check whether an exception was raised:
(receive/match ((cons (? exn? e) (? thread? sender)) (do-something...)) ...)
Casting Messages
The above
thread-call
, thread-reply
, and send-exn-to
codifies the communication signatures between threads so the correct reply can be sent back to the originator, but what if you do not need to respond back to the originator? In that case you have a "cast" pattern, and bzlib/thread
provides thread-cast
and thread-cast*
for you.
(thread-cast thd arg)
(thread-cast* thd arg arg1 ...) ;; equals (thread-cast thd (list arg arg1 ...))
They are very similar to the bare
thread-send
, except they are written with the kill-safe pattern, so they'll first attempt to wake the receiving thread if it was suspended, and then send over the arguments. Use thread-cast
or thread-cast*
if you do not need to get a response back.Applications and Getting Closer to OTP
Erlang is famous for its nine-nines uptime, and their OTP modules have a lot to do with it. It takes a lot of effort to implement OTP and get all of the bugs out, so OTP is not coming to PLT soon. But
app
which is part of bzlib/thread
is the first step toward constructing OTP for PLT.App basically provides a simple structure over the receiving thread, so you have an structure that you can pass around and manipulate. The function
make-application
tries to simplify the creation of an application:
(make-application call cast init-state)
You just need to pass the
call
function (run when triggered via thread-call
) and the cast
function (run when triggered via thread-cast
), as well as the init-state
, which are the values that the application will hold internally between the thread calls or casts.The
call
function should have the following signature:
(sender-thread passed-in-args app-state . -> . (cons/c result app-state))
The returned
app-state
do not have to be the same as the passed in app-state, but it needs to be compatible to the function for the next call.The
cast
function should have the following signature:
(passed-in-args app-state . -> . (cons/c result app-state))
The
result
will be disgarded since there isn't a response back to the sender.app-call
and app-cast
are provided as wrappers over thread-call
and thread-cast
. Unlike thread-call
and thread-cast
, app-call
and app-cast
takes variable parameter lists.
(app-call app cmd #:timeout (number? +inf.0) arg1 ...)
(app-cast app cmd arg1 ...)
cmd
is a symbol that you can use to dispatch to the correct function within your app, assuming your app provides multiple function as its APIs.That's it for now - have fun programming in erlang style in PLT Scheme.
Cool post, I'm a web developer currently learning Scheme and have had some experience with Erlang (switched from Common Lisp, it was too messy for me).
ReplyDeleteGreat post for me! Thank you :)
@Ixmatus - thanks. I plan on adding more erlang-style capabilities to PLT Scheme. Stay tuned.
ReplyDelete