Timeouts in Lisp

Timeouts are important for programs that connect to external resources or that otherwise need to bound execution time. Implementing timeouts in Lisp is possible using threads and condition variables.

(defpackage :timeouts
  (:use :cl :bordeaux-threads :cl-async))

(in-package :timeouts)

(defun main ()
  ;; For our example, suppose we're executing an external
  ;; call that returns its result after 2s. We'll use a
  ;; condition variable to simulate this.
  (let ((cv (make-condition-variable))
        (lock (make-lock))
        (result nil))
    (bt:make-thread
     (lambda ()
       (sleep 2)
       (bt:with-lock-held (lock)
         (setf result "result 1")
         (condition-variable-notify cv))))

    ;; Here we implement a timeout using cl-async's event loop.
    ;; We'll wait for either the condition variable to be notified
    ;; or for the timeout to occur.
    (as:with-event-loop (:catch-app-errors t)
      (as:delay
       (lambda ()
         (bt:with-lock-held (lock)
           (let ((cv-result
                  (bt:condition-wait
                   cv lock
                   :timeout 1)))
             (if cv-result
                 (format t "~a~%" result)
                 (format t "timeout 1~%"))))
         (as:exit-event-loop))))

    ;; If we allow a longer timeout of 3s, then the wait
    ;; for the condition variable will succeed and we'll print the result.
    (setf result nil)
    (bt:make-thread
     (lambda ()
       (sleep 2)
       (bt:with-lock-held (lock)
         (setf result "result 2")
         (condition-variable-notify cv))))

    (as:with-event-loop (:catch-app-errors t)
      (as:delay
       (lambda ()
         (bt:with-lock-held (lock)
           (let ((cv-result
                  (bt:condition-wait
                   cv lock
                   :timeout 3)))
             (if cv-result
                 (format t "~a~%" result)
                 (format t "timeout 2~%"))))
         (as:exit-event-loop))))))

Running this program shows the first operation timing out and the second succeeding.

$ sbcl --load timeouts.lisp
timeout 1
result 2

In this Lisp implementation:

  1. We use the bordeaux-threads library for threading and condition variables.
  2. The cl-async library is used for the event loop and timeouts.
  3. Instead of channels, we use condition variables and locks to communicate between threads.
  4. The select statement is replaced with condition variable waits with timeouts.
  5. We use format for printing instead of fmt.Println.

Note that Lisp doesn’t have built-in goroutines or channels, so we use threads and condition variables to achieve similar functionality. The event loop from cl-async helps us manage timeouts in a way similar to the original example.

[Next Example: Non-Blocking Channel Operations]