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 availableChannel State
(channel-empty? ch) ; No messages waiting
(channel-full? ch) ; Buffer is full (buffered channels only)
(channel-closed? ch) ; Channel has been closedClosing Channels
(channel-close! ch)
;; Closed channels:
;; - Sends fail immediately
;; - Receives return remaining buffered items, then #fSpawning 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 valueelseruns 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
- Add logging: Print task IDs and timestamps
- Start simple: Debug with one producer, one consumer
- Check for deadlocks: Ensure channels are closed
- Buffer size: Too small causes blocking, too large wastes memory