Concurrency for GoLisp
by Dave Astels
GoLisp now has a few basic concurrency primitives built on top of Go’s goroutines and channels.
I needed the ability to have some code looping in the background for something I’m working on, so I build some primitives using Go’s concurrency capabilities. It’s quite limited, but suffices for my immediate needs. Some time in the future I may add more features. Or you can.
Concurrent evaluation
The most basic capability is to be able to run code in the background.
The new fork
primitive accomplishes that. fork
takes a function
of one argument and evaluates it in a new goroutine. The argument to
the function is provided by the fork
primitive. It is an opaque
GoLisp object that wraps a Go struct that stores various bits of
information used by the concurrency primitives. I refer to this as a
process object. This object is also returned by fork
and can be
used to interact with the code running on the goroutine.
Running code in the background is that easy:
(define (run-once proc)
(write-line "start")
(sleep 1000)
(write-line "stop"))
(fork run-once)
> start
[a second goes by]
stop
[run-once completes and the goroutine terminates]
This code forks run-once
in a fire and forget manner. If we capture
the resulting process object we have the opportunity to interact in a
simple way with the forked code.
Interruptable sleeping
The above example used the standard sleep
function. There is a new
sleep function for use in forked code: proc-sleep
(short for process
sleep) that works the same except that it can be canceled. The
proc-sleep
function takes a process object in addition to the number
of milliseconds to sleep for. When it returns, the result indicates
whether it was woken prematurely (#t
) or the specified time expired
normally (#f
). You can make use of this in the forked code.
Using the process object returned by fork
other code can wake any
active proc-sleep
in the associated code by using the new wake
function:
> (define (run proc)
(do ((woken #f woken))
(woken (write-line "woken"))
(write-line "tick")
(set! woken (proc-sleep proc 10000))))
> (define p (fork run))
tick
tick
[times goes by, tick is printed every 10 seconds]
> (wake p)
woken
[run completes and the goroutine terminates]
The proc-sleep
wake functions are implemented using a channel so
waking before proc-sleep
is called, still does the expected thing:
aborting the sleep immediately.
So, that’s it for forking code and interacting with it. Next up is scheduling code evaluation.
Scheduling
To schedule a function to run at some future time, use the schedule
function. It takes a number of milliseconds and a function. The
function is evaluated on a goroutine after that many milliseconds.
schedule
returns immediately with the process object associated with
the scheduled code.
> (define (run-delayed proc)
(write-line "running"))
> (schedule 10000 run-delayed)
[10 seconds pass]
running
[run-delayed completes and the goroutine terminates]
Once again the scheduled function takes a single parameter which is the
associated process object. That means it can use proc-sleep
and any
other concurrency features we add in the future.
Once scheduled, the goroutine sits there until the time expired and
the function is evaluated. If you use the abandon
function (passing
the process object returned by ` schedule`) you can abort the
goroutine. It will stop waiting for the timeout and the scheduled
function will not be evaluated. It has no effect if used after the
schedule delay has expired.
> (define (run-delayed proc)
(write-line "running"))
> (define p (schedule 10000 run-delayed))
[5 seconds pass]
> (abandon p)
[the delay in the is cancelled and the goroutine terminates]
Closing
Well, that’s it: basic concurrency support for GoLisp. Enjoy. I look forward to seeing how this is used and extended.
Keep in mind that it’s still new and not rigerously tested, so tread carefully and let me know if you find problems.