Stateful Goroutines in Racket

Our example demonstrates how to manage state using threads and channels in Racket. This approach aligns with the idea of sharing memory by communicating and having each piece of data owned by exactly one thread.

#lang racket

(require racket/async-channel)

(struct read-op (key resp))
(struct write-op (key val resp))

(define (main)
  (define read-ops (box 0))
  (define write-ops (box 0))

  (define reads (make-async-channel))
  (define writes (make-async-channel))

  ; Thread that owns the state
  (thread
   (λ ()
     (define state (make-hash))
     (let loop ()
       (sync
        (handle-evt reads
                    (λ (read)
                      (async-channel-put (read-op-resp read)
                                         (hash-ref state (read-op-key read) 0))))
        (handle-evt writes
                    (λ (write)
                      (hash-set! state (write-op-key write) (write-op-val write))
                      (async-channel-put (write-op-resp write) #t))))
       (loop))))

  ; Start 100 reading threads
  (for ([_ (in-range 100)])
    (thread
     (λ ()
       (let loop ()
         (define read (read-op (random 5) (make-async-channel)))
         (async-channel-put reads read)
         (async-channel-get (read-op-resp read))
         (set-box! read-ops (add1 (unbox read-ops)))
         (sleep 0.001)
         (loop)))))

  ; Start 10 writing threads
  (for ([_ (in-range 10)])
    (thread
     (λ ()
       (let loop ()
         (define write (write-op (random 5) (random 100) (make-async-channel)))
         (async-channel-put writes write)
         (async-channel-get (write-op-resp write))
         (set-box! write-ops (add1 (unbox write-ops)))
         (sleep 0.001)
         (loop)))))

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

  ; Report the operation counts
  (printf "readOps: ~a~n" (unbox read-ops))
  (printf "writeOps: ~a~n" (unbox write-ops)))

(main)

In this example, we use Racket’s thread system and async channels to manage shared state. Here’s a breakdown of the key components:

  1. We define read-op and write-op structs to encapsulate read and write requests.

  2. The main function sets up the shared state and communication channels.

  3. A dedicated thread owns the state (a hash table) and processes read and write requests through async channels.

  4. We start 100 reading threads and 10 writing threads. Each thread continuously sends requests to the state-owning thread and updates the operation count.

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

To run the program, save it to a file (e.g., stateful-threads.rkt) and use the Racket interpreter:

$ racket stateful-threads.rkt
readOps: 71708
writeOps: 7177

This thread-based approach to state management can be useful in certain scenarios, especially when dealing with multiple channels or when managing multiple mutexes would be error-prone. Choose the approach that feels most natural and helps ensure the correctness of your program.