简体   繁体   中英

Common lisp: calling a class method in a separate thread

I am trying to build a common lisp implementation of the channel construct of Golang for a personal project (also to learn lisp). So far I've implemented the channels as objects of a class, containing a queue, a lock and a condition variable to signal listening functions that a new message has been added to the queue. I'm using bordeaux threads to create threads, locks, condition variables and join the executions (from the lisp cookbook ).

This is the channel class and the recive function:

(defclass channel ()
  ((messages :initform '()
             :accessor messages
             :documentation "Messages in the channel")
   (lock :initform (bt:make-lock)
         :accessor lock
         :documentation
         "Lock to push/pop messages in the channel")
   (cv :initarg :cv
       :initform (bt:make-condition-variable)
       :accessor cv
       :documentation
       "Condtional variable to notify the channel of a new message")))


(defmethod recive-loop ((self channel))
  (with-slots (lock cv messages) self
    (let ((to-ret nil))
    (loop
     (bt:with-lock-held (lock)
       (if (not (null messages))
           (setf to-ret (car (pop messages)))
           (bt:condition-wait cv lock))
       (if to-ret (return to-ret)))))))

(defmethod recive ((self channel))
  (with-slots (name thread) self
    (let ((thread
            (bt:make-thread #'(lambda() (recive-loop self))
                            :name name)))
      (bt:join-thread thread))))

(defmacro gorun (f &rest args)
  (flet ((fn () (apply f args)))
    (bt:make-thread #'fn
            :initial-bindings (list args)
            :name "gorun worker")))

gorun should be the equivalent of go routine() for go (without the light threading). To test the setup I've built a printer function over a channel

(defvar printch (channel))

(defun printover (ch)
  (let ((x (recive ch)))
    (format t "Recived variable x: ~d~%" x)))

but when I run

(gorun printover printch)

The interpreter (using sbcl , but with clisp the same happens) gives back an error:

There is no applicable method for the generic function
  #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::RECIVE (1)>
when called with arguments
  (PRINTCH).
   [Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]
See also:
  Common Lisp Hyperspec, 7.6.6 [:section]

Restarts:
 0: [RETRY] Retry calling the generic function.
 1: [ABORT] abort thread (#<THREAD "gorun worker" RUNNING {100293E9F3}>)

Backtrace:
  0: ((:METHOD NO-APPLICABLE-METHOD (T)) #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::RECIVE (1)> PRINTCH) [fast-method]
      Locals:
        SB-PCL::ARGS = (PRINTCH)
        GENERIC-FUNCTION = #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::RECIVE (1)>
  1: (SB-PCL::CALL-NO-APPLICABLE-METHOD #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::RECIVE (1)> (PRINTCH))
      Locals:
        ARGS = (PRINTCH)
        GF = #<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::RECIVE (1)>
  2: (PRINTOVER PRINTCH)
      Locals:
        CH = PRINTCH
  3: ((LAMBDA NIL :IN BORDEAUX-THREADS::BINDING-DEFAULT-SPECIALS))
      [No Locals]

I'm confused, since the method to run over the channel printch should be the one I've defined.

Trying to call a class method inside of a new thread, but got no applicable method

A macros is supposed to return code to run in place of the original call. Your macro is creating the thread at expansion time.

If you're not using backtick in a macro definition, there's usually something wrong with it. You should figure out what the code would look like without the macro, then define a macro that returns code with that same structure in a backticked list, replacing the places that need to vary with the parameters, using comma to expand them.

(defmacro gorun (f &rest args)
  `(bt:make-thread (function ,f)
        :initial-bindings (list ,@args)
        :name "gorun worker"))

In the above, you need to substitute the function name into a (function...) expression, and the args list as the :initial-bindings argument.

In multi-threaded environments it is often the case that special variables are thread-local. The global binding is visible from all threads, but if you bind one locally its value will not automatically be transferred to a thread created in that context. It has to be done explicitly, and I wrote a pair of macros recently just for that.

The first one captures bindings into lexically-scoped variables; the other bind the original variables back to the values captured in a different context.

I am using an intermediate data structure in the code to store bindings:

(defstruct bindings data)

The first macro is with-captured-bindings :

(defmacro with-captured-bindings ((&rest symbols) as name &body body)
  (assert (eq as :as))
  (loop for s in (alexandria:flatten
                  (sublis
                   '((:stdio *standard-output* *error-output* *standard-input*)
                     (:path *default-pathname-defaults*))
                   symbols))
        for g = (gensym)
        collect (list g s) into capture
        collect (list s g) into rebind
        finally
           (return
             `(let ,capture
                ,@(subst (make-bindings :data rebind)
                         name
                         body)))))

The capture variable holds a list of bindings to initialize the lexically-scoped variables. The rebind variables is a list of bindings to set back special variables to their values in another thread.

I inject with subst an instance of the bindings struct in the code. It helps to have a dedicated data structure, but the crude search-and-replace approach means the symbols name will not be usable as a function, local macro, etc. in body . I don't think it is too much of a problem.

Also, I define aliases like :stdio and :path for commonly used variables.

The second macro is with-bindings :

(defmacro with-bindings (bindings &body body)
  (check-type bindings bindings)
  `(let ,(bindings-data bindings)
     ,@body))

This replaces the intermediate struct with the proper code. The final code does not have this struct anymore and can be processed as usual.

For example:

(defvar *my-var* "hello")

(with-captured-bindings (:stdio :path *my-var*) :as <bindings>
  (sb-thread:make-thread 
   (lambda ()
     (with-bindings <bindings>
       (print *var*)))))

A first application of macroexpand gives:

(LET ((#:G3882 *STANDARD-OUTPUT*)
      (#:G3883 *ERROR-OUTPUT*)
      (#:G3884 *STANDARD-INPUT*)
      (#:G3885 *DEFAULT-PATHNAME-DEFAULTS*)
      (#:G3886 *MY-VAR*))
  (SB-THREAD:MAKE-THREAD
   (LAMBDA ()
     (WITH-BINDINGS #S(BINDINGS
                       :DATA ((*STANDARD-OUTPUT* #:G3882)
                              (*ERROR-OUTPUT* #:G3883)
                              (*STANDARD-INPUT* #:G3884)
                              (*DEFAULT-PATHNAME-DEFAULTS* #:G3885)
                              (*MY-VAR* #:G3886)))
       (PRINT *MY-VAR*)))))

Notice that there is #S(BINDINGS...) object in the tree.

The full expansion is:

(LET ((#:G3887 *STANDARD-OUTPUT*)
      (#:G3888 *ERROR-OUTPUT*)
      (#:G3889 *STANDARD-INPUT*)
      (#:G3890 *DEFAULT-PATHNAME-DEFAULTS*)
      (#:G3891 *MY-VAR*))
  (SB-THREAD:MAKE-THREAD
   (LAMBDA ()
     (LET ((*STANDARD-OUTPUT* #:G3887)
           (*ERROR-OUTPUT* #:G3888)
           (*STANDARD-INPUT* #:G3889)
           (*DEFAULT-PATHNAME-DEFAULTS* #:G3890)
           (*MY-VAR* #:G3891))
       (PRINT *MY-VAR*)))))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM