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
receiveis pattern match enabled, and we want something similar in PLT scheme.
bzlib/threadprovides the pattern match enabled receive for PLT Scheme, with the syntax
(require (planet blib/thread)) ;; load the package.
;; default syntax - with pattern match
(match-pattern exp ...) ...)
;; timer syntax - just the timer.
(after time exp ...))
;; extended syntax - combine the pattern match with timer.
(match-pattern exp ...) ...
(after time exp ...))
The match-pattern has the exact same syntax as
scheme/matchprovides the underlying matching capability. You can also specify an
afterclause 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
afterclause 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/matchis enhanced with a
;; with the sync clause - it is also pattern-match enabled...
(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
syncclause 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.
receive/matchform the basis of multi-threads communications.
Communication Bewteen Multiple Threads
As stated earlier, the challenge with the bare
thread-receiveis that you have to handle your own message dispatching if there are multiple threads trying to communicate with each other. While
receive/matchprovides 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/matchmatches the signature:
(receive/match ((list (? thread? thd) args) (do-whatever) ...) ...)
bzlib/threadcodifies 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/matchand extracted your args for processing, you can then use
thread-replyto 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-toto 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...)) ...)
send-exn-tocodifies 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
(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*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
appwhich is part of
bzlib/threadis 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-applicationtries to simplify the creation of an application:
(make-application call cast init-state)
You just need to pass the
callfunction (run when triggered via
thread-call) and the
castfunction (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.
callfunction should have the following signature:
(sender-thread passed-in-args app-state . -> . (cons/c result app-state))
app-statedo 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.
castfunction should have the following signature:
(passed-in-args app-state . -> . (cons/c result app-state))
resultwill be disgarded since there isn't a response back to the sender.
app-castare provided as wrappers over
app-casttakes variable parameter lists.
(app-call app cmd #:timeout (number? +inf.0) arg1 ...)
(app-cast app cmd arg1 ...)
cmdis 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.