Thread-safe memoize function

Nikita <[email protected]> Wed, 22 Aug 2007 11:39:37 +0300
Newsgroups gmane.comp.java.sisc.user
Message-ID <[email protected]>
Hello,


I've been implementing thread-safe caching function. And I would like to 
know your opinion how this implementation is safe.

It uses mutexes to lock cache hash-table, but cache is not locked for 
the whole time of function call. If the result was not found in cache, 
then new mutex is put into cache. Than cache table is unlocked and 
result of function of given arguments is evaluated, result then is put 
into table without locking the table. If some other thread wants a 
result of the same arguments, then it receives mutex from cache and 
waits for it being unlocked.


I'm sorry, if the given description unclear because of my poor English. 
But I hope the code will be easier to understand.

(import threading)
(require-library 'sisc/libs/srfi/srfi-19) ;call-with-values
(import srfi-19)

(require-library 'sisc/libs/srfi/srfi-69) ;hash-table
(import srfi-69)
(define (memoize fn)
  (let* ((cache (make-hash-table 'equal))
         (nothing "nothing")
         (cache-mutex (mutex-of cache)))
    (define memoized
      (lambda args
        ;; lock table for the time when we get cached result.
        (mutex/lock! cache-mutex)
        (let ((result (hash-table-ref cache
                                      args
                                      (lambda () nothing))))
          (cond ((eq? result nothing)
                 (let ((mutex (mutex/new)))
                   (mutex/synchronize mutex
                     (lambda ()
                       (hash-table-set! cache args mutex)
                       ;; unlock the table for the time of evaluation,
                       ;; threads which will try to get result of the same
                       ;; arguments will get mutex and will be waiting
                       ;; for its lock to be released (see the next cond 
case)
                       (mutex/unlock! cache-mutex)
                       (call-with-values (lambda () (apply fn args))
                                         (lambda result
                                           (hash-table-set! cache args 
result)
                                           (apply values result)))))))
                ;; If we have got a mutex as a result then it means 
function now is being evaluated
                ;; in another thread. In this case we just wait 
patiently for mutex to be unlocked
                ;; and try to get the result once again
                ((mutex? result)
                 (mutex/unlock! cache-mutex)
                 ;;(display (format "waiting for the result of ~a to be 
computed in another thread\n" args))
                 (mutex/synchronize result
                   (lambda ()
                     ;;(display "lock acquired, which means that the 
result has been computed in the other thread, trying again\n")
                     (void)))
                 (apply memoized args))
                (else
                 (mutex/unlock! cache-mutex)
                 ;;(display (format "value for args: ~a was found in 
cache: ~a\n" args result))
                 (apply values result))))))
    memoized))


Here is also code to test this function:

(define (fun arg)
  (display (format "(fun ~a) is called\n" arg))
  (sleep 500)
  arg)

(define memo-fun (memoize fun))

(define (count max call-back)
  (define (loop i)
    (when (< i max)
      (call-back i)
      (loop (+ i 1))))
  (loop 0))

(count 3 (lambda (i)
            (thread/spawn (lambda ()
                            (display (format "thread ~a started\n" i))
                            (count 3 (lambda (j)
                                        (display (format "thread ~a 
(memo-fun ~a) = ~a\n" i j (memo-fun j)))))
                            (display (format "thread ~a finished\n" i))))))


To get more output of how it works uncomment calls to display in the 
definition of memoize.


So, what do you think about this implementation? Is it really 
thread-safe? Is it optimal?






PS: In the code above I used 'count' function instead of 'do' because I 
had problems with it. When I tried simple example of how to use 'do' 
from R5RS I got the following:

W;>(do ((vec (make-vector 5))
     (i 0 (+ i 1)))
    ((= i 5) vec)
  (vector-set! vec i i))
Error: undefined variable '|\u00a0\u00a0|'.

Guile also produces an error, while plt-scheme


Best regards,
 Nikita

-------------------------------------------------------------------------
This SF.net email is sponsored by: Splunk Inc.
Still grepping through log files to find problems?  Stop.
Now Search log events and configuration files using AJAX and a browser.
Download your FREE copy of Splunk now >>  http://get.splunk.com/