Help test via snapshots: parallel threads

Oh, that is VERY helpful. Thanks for the explanation! It seems like parallel threads have a number of advantages and the code to use futures vs. parallel threads is almost identical.

1 Like

The #:keep 'results argument makes thread very similar to delay/thread (and, by extension, delay/sync or delay/idle). The big difference I see is that the promises catch exceptions and raise them when forces, whereas thread uses the exception handler, and a thread that ended in an exception will have (void) as its result, like a #:keep #f thread:

> (list (thread-wait (thread #:keep 'results (lambda () (+ "oops")))))
+: contract violation
  expected: number?
  given: "oops"
 [,bt for context]
'(#<void>)

Maybe this makes sense: catching exceptions might be expensive, and maybe it's useful to have this as a lower-level mechanism. On the other hand, I'd guess catching exceptions is a relatively common desideratum, and maybe the implementation of thread is in a position to implement it especially efficiently, or just conveniently. Potentially a #:keep result/exn option could be added later.

It doesn't seem like there's a way to tell currently (without controlling the thread's thunk) if a thread terminated normally, with an exception, or by being killed.

Eventually, it would be nice to support parallelism for delay/thread and friends.

Finally, the existence of delay/idle led me to notice that the docs for system-idle-evt probably need an update for parallel threads:

Returns an event that is ready for synchronization when the system is otherwise idle: if the result event were replaced by never-evt, no thread in the system would be available to run. In other words, all threads must be suspended or blocked on events with timeouts that have not yet expired.

From this experiment, it looks like parallel threads must also be blocked, but I'm not sure that behavior is desirable:

> (define stop? #f)
> (define n 0)
> (thread #:pool 'own (lambda ()                                 
                        (let loop ()                           
                          (if stop?                           
                              (displayln n)      
                              (begin (set! n (add1 n))
                                     (loop))))))
#<thread>
> (list n (sync (system-idle-evt)) n (set! stop? #t) n)
^Cuser break [,bt for context]

On the other hand, syncing on (system-idle-evt) in a parallel thread seems to work fine:

> (sync (thread #:pool 'own
                #:keep 'results
                (lambda ()
                  (sync (system-idle-evt))
                  1)))
#<thread>

Yes, I agree. I intended to add that, but haven't gotten to it.

The updated thread-wait does support this with an extra fail-thunk argument:

> (list (thread-wait (thread #:keep 'results (lambda () (+ "oops"))) 
                     (lambda () 'failed)))
+: contract violation
  expected: number?
  given: "oops"
 [,bt for context]
'(failed)

The intent is definitely that (system-idle-evt) waits until no parallel thread can make progress, the same a coroutine threads. That's important for the only use that I know for system-idle-evt, which is testing.

Thanks, I'd missed the change to thread-wait!

The docs made it clear how to distinguish normal termination from an exception, but I wasn't sure until trying the following experiment that the failure thunk is also called if the thread is killed. Is being killed an instance of “or otherwise aborted to the thread’s initial prompt”, or is it a different category? I'll try to propose a change to the docs.

$ ./bin/racket 
Welcome to Racket v8.18.0.19-2025-10-12-72b9c65d3f [cs].
> (define sema (make-semaphore))
> (define thd   
    (thread
     #:keep 'results
     (lambda ()     
       (sync sema)
       'normal)))
> (kill-thread thd)
> (thread-wait thd (lambda () 'exn))
'exn

I have low confidence that my understanding and use of system-idle-evt was ever correct, but one way I have used it is for low-priority periodic background tasks. Instead of just repeating the job every N seconds, I'd try, within some bounds, to have it run when the system wasn't otherwise doing anything. Something like this:

#lang racket
(define (launch-background-task min-delay max-delay thunk)
  (thread (λ ()
            (let loop ()
              (sleep min-delay)
              (sync/timeout (- max-delay min-delay)
                (system-idle-evt))
              (thunk)
              (loop)))))

In this case, I only care that no coroutine threads time-sharing with this one can make progress, since blocking the background task wouldn't make any more resources available to other OS threads. I've used this pattern in programs that also use places, and I definitely would have been surprised if anything happening in a different place mattered to (system-idle-evt), but I haven't checked that understanding. I have never used futures beyond toy experiments, so I'd never given much thought to how they would interact with (system-idle-evt), but the fact that its documentation talks only about “threads” gave me the impression that a future being able to run would not matter.

It's also possible that some other construct, maybe thread groups, could be a better way to express what I wanted. Thread groups are another feature I've never seriously used. The model of a tree of threads/groups sharing equal priority at each level of nesting is conceptually elegant, and I can see how it maps very neatly onto e.g. programs running in DrRacket, but it hasn't corresponded in an obvious way to problems I've worked on, where there aren't clear “groups” so much as a job, or a few, that is especially low priority. Maybe, if an application anticipated a several such jobs, a library could provide a low-priority-thread-group that components could put their background jobs into, but it would rely on components to explicitly cooperate (since putting just one thread in the group wouldn't deprioritize it), which seems less than ideal from a modularity perspective.

Yes, when a thread is killed, thread-wait treats it like one that aborted to the initial prompt. I'll update the documentation.

As for system-idle-evt, I agree that you're looking for something like thread priorities, which Racket doesn't currently provide. And I agree that you've managed to get something like low-priority scheduling out of (system-idle-evt) by knowing how all the threads within your place behave. You're also right that futures do not affect whether (system-idle-evt) is ready.

But system-idle-evt is not intended for low-priority scheduling, and it achieves that goal only in a limited sense (e.g., (thunk) runs with the same priority as everything else in your example). Instead, the system-idle-evt constructor is intended for testing related to thread thread synchronization API, where the test needs to wait for threads in flight to reach a stopping point. For that purpose, it's most useful to thread all threads the same (since it's about the thread API), even if there are futures and places running.

I'm not sure whether it would be generally useful to have a different primitive that is like system-idle-evt but, say, specific to a thread group. I'm pretty sure that it would be useful for testing purposes to have something like system-idle-evt that also covers futures and places, but that need is small enough that it has never seemed worthwhile. More support for prioritization also seems useful to add eventually.