common-lisp
CommonLisp
lisp
LispDay 11

処理の途中経過を保持して外部に隔離する

この記事はLisp Advent Calendar 2017の11日目の記事です。

はじめに

現行のDarkmatterについてはこちらをご覧ください.
開発版のDarkmatter Core/Notebookについては最低限の動作が出来たら追記します.

現在開発中のDarkmatter Core/Notebookには,コード実行の途中経過を手動で保存する機能があります.
そのため実行を中断しても途中の結果が見れたり,実行時エラーで死んだりしても途中経過を残しておけば確認が可能です.
さらにリアルタイムな可視化にも利用されるため,機械学習の実験などに有用です.
途中経過を保存する領域は単なるhash-tableなので,通常の変数と同様に扱うことが出来ます.

トリッキーな実装なので誰得かは分かりませんが,記録として残しておきます.

実装概要

  • コードの実行はthreadを立ち上げて行う
  • threadと途中経過を保存する領域(context)を紐付けるtask構造体を作る
  • threadで実行するコードを,contextを受け取る1変数関数(lambda)に変換する
  • thread立ち上げ前にcontextを作成し,作成した1変数関数に適用して実行を開始

(ここに図を入れたかった...)

最低限の実装

(require :bordeaux-threads)

(defstruct task
  (thread nil)
  (context nil))

(defun create-task (fun &optional (context nil))
  (make-task
    :thread (bt:make-thread fun)
    :context context))

(defun %read-from-string-with-context (str)
  (read-from-string
    (format nil "(lambda (%context%) (declare (ignorable %context%)) ~A)" str)))

(defun spawn (code)
  (let ((source (%read-from-string-with-context code))
        (context (make-hash-table)))
    (create-task
      (lambda () (funcall (eval source) context))
      context)))

(let ((task (spawn "(loop for i from 0 to 100 do (setf (gethash :count %context%) i) (sleep 1))")))
  (sleep 1)
  (format t "~A~%" (gethash :count (task-context task)))
  (sleep 5)
  (format t "~A~%" (gethash :count (task-context task)))
  task)

実行結果は以下のようになります.

0
5
#S(TASK
   :THREAD #<SB-THREAD:THREAD "Anonymous thread" RUNNING {1002CF6143}>
   :CONTEXT #<HASH-TABLE :TEST EQL :COUNT 1 {1002CF6023}>)

Darkmatter Coreではtaskの実行結果とcontextをクライアントからのリクエストに応じて送信するようにしています.
実際のコードは下のURLにあります.

まとめ

実際にDarkmatter Coreで使われているテクニック(?)を紹介しました.
contextをグローバル変数として持っていても悪くは無いのですが,暗黙の一時変数のように使いたかったのでこのようなやり方になりました.

Darkmatter Core/NotebookはDarkmatterをコード実行サーバとWebアプリケーションに分割し,コードベースを捨て去って一から書き直しているものです.
これはリファクタリングと少しの修正を含んでいて,基本的にDarkmatterとの互換性はありません.
Darkmatterで利用していた形式のノートブックファイルの引き継ぎ措置は用意するつもりなので,今はDarkmatterの方を使うようにしてください.