Concurrency for GoLisp

31 Dec 2014

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.