なんだこれは

はてなダイアリーから移転しました。

Common Lisp でマルチスレッド

どうしよう、超奔放凶暴な本性を

Common Lisp でマルチスレッド

仕事で、後輩がマルチスレッドの勉強することになったらしい。その話はもちろん、Common Lisp ではなく、C言語の話だったらしいが。

さて帰宅しながら、Common Lispでのマルチスレッドはどうだったかなと復習をすることにした。

もちろん、手抜きなので、Common Lisp Cockbookのプロセス を眺めることにする。 面白いのは、Lispがあまりにも古い言語なので、実装によってはスレッドをプロセスと呼んでいるとあることでしたね。 私はCommon Lispの実装にはよく、ClozureCL を使っているのですが、 Clozure CL Documentationのスレッド概要APIの名前には process が残っている みたいに書いてあるのはそういうことなんですね。

Wherever possible, I'll try to use the term "thread" to denote a lisp thread, even though many of the functions in the API have the word "process" in their name. https://ccl.clozure.com/manual/chapter7.1.html#Threads-overview

環境

  • ClozureCL
  • quicklisp
    • bordeaux-threads

準備

Common Lisp の REPL で quciklisp で bt-semaphore をいれればいいらしいです。

CL-USER> (ql:quickload "bt-semaphore")

これで T が返ればスレッドがサポートできるらしいです。

CL-USER> bt:*supports-threads-p*
T

全てのスレッドを取得する手続き

(bt:all-threads)

スレッドを作る(即実行)

bt:make-thread の第一引数に引数を取らないラムダを渡してやればいいらしい。そしてスレッドを作成すると即実行できるとのこと。

(bt:make-thread (lambda () body))

注意点 format

別スレッドの中からは、(format t ...)でトップレベルの標準出力に出力できないらしい。そのため、トップレベルの *standard-output* をシンボルにバインドしておくか、その環境を渡すとよいとのこと。

(defun print-message-top-level-fixed ()
  (let ((top-level *standard-output*))
    (bt:make-thread
       (lambda ()
          (format top-level "Hello from thread!"))
       :name "hello"))
  nil)

メイン以外のスレッドから *counter* を更新する例

(defparameter *counter* 0)

(defun reset () (setf *counter* 0))

(defun test-update-global-variable ()
  (bt:make-thread
   (lambda ()
     (sleep 1)
     (incf *counter*)))
  *counter*)

別々のスレッドから *counter* を更新する例

これは、ロックがないから、結果がめちゃくちゃになる例。そういえば、sleep の引数は秒単位だけど、整数でなくてもよいのだった。

(defun countup ()
    (incf *counter*))
(defun countdown ()
    (decf *counter*))

(defun updown (name &optional (stream *standard-output*) )
  (format stream "task ~a counter ~a ~%" name *counter*)
  (countup)
  (sleep 0.01)
  (countdown))

(defun task1 (name &optional (stream *standard-output*) )
  (loop repeat 1000 do (updown name stream))
  (format stream "task: ~a , counter: ~a ~%" name *counter*))

(defun task-runner (name)
  (let ((stream *standard-output*))
    (format t "start taks name: ~a ~%" name)
    (bt:make-thread (lambda () (task1 name stream)))))

(defun multithread-task-runner ()
  (task-runner 1)
  (task-runner 2)
  (task-runner 3)
  (task-runner 4)
  (task-runner 5)
  (format t "TOTAL : ~a ~%" *counter*)
  )

ロックをいれてみる例

(bt:make-lock) で ロックを作成して、ロックを使う箇所を(bt:with-lock-held (<ロック>) ... ) のように包めばいいのね。

(defvar *lock* (bt:make-lock))

(defun countup ()
  (bt:with-lock-held (*lock*) (incf *counter*)))
  
(defun countdown ()
  (bt:with-lock-held (*lock*) (decf *counter*)))