In object-oriented programming an object is active when its state depends on clock. Usually an active object encapsulates a task that updates the object's state. To the outer world the object looks like a normal object with methods that can be called from outside. Implementation of such methods must have a certain synchronization mechanism with the encapsulated task in order to prevent object's state corruption.

A typical instance of an active object is an animation widget. The widget state changes with the time, while as an object it has all properties of a normal widget.

The task

Implement an active integrator object. The object has an input and output. The input can be set using the method Input. The input is a function of time. The output can be queried using the method Output. The object integrates its input over the time and the result becomes the object's output. So if the input is K(t) and the output is S, the object state S is changed to S + (K(t1) + K(t0)) * (t1 - t0) / 2, i.e. it integrates K using the trapeze method. Initially K is constant 0 and S is 0.

In order to test the object:

 set its input to sin (2π f t), where the frequency f=0.5Hz. The phase is irrelevant.
 wait 2s
 set the input to constant 0
 wait 0.5s
Verify that now the object's output is approximately 0 (the sine has the period of 2s). The accuracy of the result will depend on the OS scheduler time slicing and the accuracy of the clock.

 
(defclass integrator ()
  ((input :initarg :input :writer input :reader %input)
   (lock :initform (bt:make-lock) :reader lock)
   (start-time :initform (get-internal-real-time) :reader start-time)
   (interval :initarg :interval :reader interval)
   (thread :reader thread :writer %set-thread)
   (area :reader area :initform 0 :accessor %area)))
 
(defmethod shared-initialize
    ((integrator integrator) slot-names &key (interval nil interval-s-p) &allow-other-keys)
  (declare (ignore interval))
  (cond
    ;; Restart the thread if any unsynchronized slots are
    ;; being initialized
    ((or
      (eql slot-names t)
      (member 'thread slot-names)
      (member 'interval slot-names)
      (member 'start-time slot-names)
      (member 'lock slot-names)
      interval-s-p)
     ;; If the instance already has a thread, stop it and wait for it
     ;; to stop before initializing any slots
     (when (slot-boundp integrator 'thread)
       (input nil integrator)
       (bt:join-thread (thread integrator)))
     (call-next-method)
     (let* ((now (get-internal-real-time))
            (current-value (funcall (%input integrator) (- (start-time integrator) now))))
       (%set-thread
        (bt:make-thread
         (lambda ()
           (loop
             ;; Sleep for the amount required to reach the next interval;
             ;; mitigates drift from theoretical interval times
             (sleep
              (mod
               (/ (- (start-time integrator) (get-internal-real-time))
                  internal-time-units-per-second)
               (interval integrator)))
             (let* ((input
                      (bt:with-lock-held ((lock integrator))
                        ;; If input is nil, exit the thread
                        (or (%input integrator) (return))))
                    (previous-time (shiftf now (get-internal-real-time)))
                    (previous-value
                      (shiftf
                       current-value
                       (funcall input (/ (- now (start-time integrator)) internal-time-units-per-second)))))
               (bt:with-lock-held ((lock integrator))
                 (incf (%area integrator)
                       (*
                        (/ (- now previous-time)
                           internal-time-units-per-second)
                        (/ (+ previous-value current-value)
                           2)))))))
         :name "integrator-thread")
        integrator)))
    (t
     ;; If lock is not in SLOT-NAMES, it must already be initialized,
     ;; so it can be taken while slots synchronized to it are set
     (bt:with-lock-held ((lock integrator))
       (call-next-method)))))
 
(defmethod input :around (new-value (integrator integrator))
  (bt:with-lock-held ((lock integrator))
    (call-next-method)))
 
(defmethod area :around ((integrator integrator))
  (bt:with-lock-held ((lock integrator))
    (call-next-method)))
 
(let ((integrator
        (make-instance 'integrator
                       :input (lambda (time) (sin (* 2 pi 0.5 time)))
                       :interval 1/1000)))
  (unwind-protect
       (progn
         (sleep 2)
         (input (constantly 0) integrator)
         (sleep 0.5)
         (format t "~F~%" (area integrator)))
    (input nil integrator)))
 