Stateful Goroutines in Scheme

In this example, we’ll explore how to manage state using message passing and processes in Scheme. This approach aligns with the functional programming paradigm and provides a way to handle shared state without explicit locking mechanisms.

(import (scheme base)
        (scheme write)
        (srfi 18))  ; For threading support

;; Define structures for read and write operations
(define-record-type <read-op>
  (make-read-op key resp)
  read-op?
  (key read-op-key)
  (resp read-op-resp))

(define-record-type <write-op>
  (make-write-op key val resp)
  write-op?
  (key write-op-key)
  (val write-op-val)
  (resp write-op-resp))

;; Main function
(define (main)
  (let ((read-ops 0)
        (write-ops 0)
        (reads (make-channel))
        (writes (make-channel)))

    ;; Start the state-managing thread
    (thread-start!
     (lambda ()
       (let ((state (make-hash-table)))
         (let loop ()
           (let ((msg (channel-get (choice-select reads writes))))
             (cond
              ((read-op? msg)
               (let ((value (hash-table-ref/default state (read-op-key msg) 0)))
                 (channel-put (read-op-resp msg) value)))
              ((write-op? msg)
               (hash-table-set! state (write-op-key msg) (write-op-val msg))
               (channel-put (write-op-resp msg) #t))))
           (loop)))))

    ;; Start 100 read threads
    (do ((i 0 (+ i 1))) ((= i 100))
      (thread-start!
       (lambda ()
         (let loop ()
           (let* ((key (random 5))
                  (resp (make-channel))
                  (read (make-read-op key resp)))
             (channel-put reads read)
             (channel-get resp)
             (set! read-ops (+ read-ops 1))
             (thread-sleep! 0.001))
           (loop)))))

    ;; Start 10 write threads
    (do ((i 0 (+ i 1))) ((= i 10))
      (thread-start!
       (lambda ()
         (let loop ()
           (let* ((key (random 5))
                  (val (random 100))
                  (resp (make-channel))
                  (write (make-write-op key val resp)))
             (channel-put writes write)
             (channel-get resp)
             (set! write-ops (+ write-ops 1))
             (thread-sleep! 0.001))
           (loop)))))

    ;; Let the threads work for a second
    (thread-sleep! 1)

    ;; Report the operation counts
    (display "readOps: ") (display read-ops) (newline)
    (display "writeOps: ") (display write-ops) (newline)))

;; Run the main function
(main)

This Scheme program demonstrates a concurrent approach to managing shared state using message passing and threads. Here’s a breakdown of the key components:

  1. We define record types <read-op> and <write-op> to encapsulate read and write requests.

  2. The main function sets up channels for reads and writes, and starts a state-managing thread.

  3. The state-managing thread maintains a hash table and processes read and write requests received through channels.

  4. We start 100 read threads and 10 write threads, each continuously sending requests to the state-managing thread.

  5. After letting the threads run for a second, we report the total number of read and write operations performed.

This approach ensures that the shared state (the hash table) is accessed only by a single thread, preventing race conditions without using explicit locks. Other threads communicate with the state-managing thread via message passing, which aligns with functional programming principles.

Running this program might produce output similar to:

readOps: 71708
writeOps: 7177

The exact numbers may vary due to the concurrent nature of the program. This approach, while more complex than using explicit locks, can be beneficial in scenarios involving multiple channels or when managing multiple shared resources would be error-prone. Choose the approach that feels most natural and helps ensure the correctness of your program.