sigildocs

Concurrency Guide

Sigil provides cooperative concurrency through channels and lightweight tasks.

Channels

Channels are the primary communication mechanism between concurrent tasks.

Creating Channels

(import (sigil channels))

;; Unbuffered channel - sends block until received
(define ch (make-channel))

;; Buffered channel - can hold up to 10 items
(define buffered-ch (make-channel 10))

Sending and Receiving

;; Send a value (may block if unbuffered/full)
(channel-send ch "hello")

;; Receive a value (blocks until available)
(define msg (channel-receive ch))

;; Non-blocking receive
(define msg (channel-try-receive ch))
; Returns #f if no message available

Channel State

(channel-empty? ch)   ; No messages waiting
(channel-full? ch)    ; Buffer is full (buffered channels only)
(channel-closed? ch)  ; Channel has been closed

Closing Channels

(channel-close! ch)

;; Closed channels:
;; - Sends fail immediately
;; - Receives return remaining buffered items, then #f

Spawning Tasks

Use go to spawn concurrent tasks:

(import (sigil channels))

(define ch (make-channel 5))

;; Spawn a producer
(go
  (let loop ((n 0))
    (when (< n 10)
      (channel-send ch n)
      (loop (+ n 1))))
  (channel-close! ch))

;; Consume in main task
(let loop ()
  (let ((val (channel-receive ch)))
    (when val
      (display val)
      (newline)
      (loop))))

Cooperative Scheduling

Sigil uses cooperative multitasking. Tasks yield at:

  • Channel operations (send/receive)
  • Explicit yields
  • I/O operations

Tasks that don't yield will block others:

;; Bad: never yields
(go
  (let loop () (loop)))  ; Blocks everything!

;; Good: yields periodically
(go
  (let loop ()
    (do-work)
    (yield)
    (loop)))

Common Patterns

Producer-Consumer

(define work-queue (make-channel 100))

;; Producer
(go
  (for-each
    (lambda (item)
      (channel-send work-queue item))
    items)
  (channel-close! work-queue))

;; Consumer
(let loop ()
  (let ((item (channel-receive work-queue)))
    (when item
      (process item)
      (loop))))

Fan-Out (One to Many)

(define (broadcast ch workers)
  (let loop ()
    (let ((msg (channel-receive ch)))
      (when msg
        (for-each
          (lambda (w) (channel-send w msg))
          workers)
        (loop)))))

;; Create workers
(define worker-channels
  (map (lambda (_) (make-channel 10))
       '(1 2 3)))

;; Spawn workers
(for-each
  (lambda (ch)
    (go
      (let loop ()
        (let ((work (channel-receive ch)))
          (when work
            (process-work work)
            (loop))))))
  worker-channels)

;; Broadcast work
(broadcast main-channel worker-channels)

Fan-In (Many to One)

(define results (make-channel 100))

;; Spawn multiple workers that send to same channel
(for-each
  (lambda (task)
    (go
      (channel-send results (compute task))))
  tasks)

;; Collect results
(let loop ((count 0) (acc '()))
  (if (= count (length tasks))
      acc
      (loop (+ count 1)
            (cons (channel-receive results) acc))))

Select (Multiplexing Channels)

Use channel-select to handle multiple channels without blocking:

(define ch1 (make-channel 1))
(define ch2 (make-channel 1))

;; Non-blocking select on multiple channels
(channel-select
  ((recv ch1) => (lambda (val) (handle-ch1 val)))
  ((recv ch2) => (lambda (val) (handle-ch2 val)))
  (else (display "nothing ready")))

;; Can also select on sends
(channel-select
  ((send ch1 "hello") => (lambda () (display "sent to ch1")))
  ((recv ch2) => (lambda (val) (display val)))
  (else #f))

The channel-select macro tries each clause in order:

  • (recv ch) succeeds if a value is available
  • (send ch val) succeeds if the channel can accept a value
  • else runs if no channel operation succeeds

Event-Driven Architecture

Channels work well for event systems:

(define event-bus (make-channel 100))

;; Event types
(define (make-event type data)
  (cons type data))

(define (event-type e) (car e))
(define (event-data e) (cdr e))

;; Publish
(define (publish type data)
  (channel-send event-bus (make-event type data)))

;; Subscribe (filter by type)
(define (subscribe types handler)
  (go
    (let loop ()
      (let ((event (channel-receive event-bus)))
        (when event
          (when (memq (event-type event) types)
            (handler event))
          (loop))))))

;; Usage
(subscribe '(user-login user-logout)
  (lambda (e)
    (log-event (event-type e) (event-data e))))

(publish 'user-login "alice")

Example: Chat Server

See examples/chat-server.sgl for a complete example using:

  • A main event channel
  • Multiple concurrent client handlers
  • Broadcast to all connected clients

Key patterns from that example:

;; Accept connections in a loop
(go
  (let loop ()
    (let ((client (tcp-accept server)))
      (when client
        (spawn-client-handler client)
        (loop)))))

;; Handle each client concurrently
(define (spawn-client-handler client)
  (go
    (let loop ()
      (let ((msg (socket-read-line client)))
        (when (not (eof-object? msg))
          (broadcast-message msg)
          (loop))))))

Best Practices

1. Use Buffered Channels for Performance

;; Unbuffered: every send blocks
(define slow-ch (make-channel))

;; Buffered: sends don't block until full
(define fast-ch (make-channel 100))

2. Always Close Channels

;; Producer closes when done
(go
  (produce-items ch)
  (channel-close! ch))  ; Signal completion

;; Consumer checks for close
(let loop ()
  (let ((item (channel-receive ch)))
    (when item  ; #f means closed
      (process item)
      (loop))))

3. Avoid Shared Mutable State

;; Bad: shared variable
(define counter 0)
(go (set! counter (+ counter 1)))  ; Race condition!

;; Good: communicate via channels
(define counter-ch (make-channel))
(go
  (let loop ((count 0))
    (channel-send counter-ch count)
    (loop (+ count 1))))

4. Handle Errors in Tasks

(go
  (guard (err (else (log-error err)))
    (risky-operation)))

5. Limit Concurrency

;; Semaphore pattern - limit to N concurrent tasks
(define (make-semaphore n)
  (let ((ch (make-channel n)))
    (let loop ((i 0))
      (when (< i n)
        (channel-send ch #t)
        (loop (+ i 1))))
    ch))

(define (with-semaphore sem thunk)
  (channel-receive sem)  ; Acquire
  (let ((result (thunk)))
    (channel-send sem #t)  ; Release
    result))

(define sem (make-semaphore 5))
(go (with-semaphore sem (lambda () (heavy-work))))

Debugging Concurrent Code

  1. Add logging: Print task IDs and timestamps
  2. Start simple: Debug with one producer, one consumer
  3. Check for deadlocks: Ensure channels are closed
  4. Buffer size: Too small causes blocking, too large wastes memory