How to implement parallel-or?

The parallel-or operation allows you to run two computations in parallel and either immediately returns true if one of the computations halts with true or returns false if both computations halt with false.

I couldn't figure out a way of doing this with future and touch and I wonder if I missed something or there are other primitives I'm supposed to use.

2 Likes

This is difficult to do with futures, because futures can potentially block, making no more parallel progress until touched, and there is no mechanism analogous to choice-evt to select whichever future is ready first.

But the new parallel threads coming in Racket 9.0 (test a candidate from https://pre-release.racket-lang.org!) will make parallel-or very convenient to express:

#lang racket

(module+ main
  (parallel-or (begin (println 'a) 'a)
               (begin (println 'b) 'b)))

(define-syntax-rule (parallel-or a-expr b-expr)
  (do-parallel-or (λ () a-expr) (λ () b-expr)))

(define (do-parallel-or a-thunk b-thunk)
  (define (spawn thunk)
    (define cust (make-custodian))
    (values (parameterize ([current-custodian cust])
              (thread #:pool 'own #:keep 'results thunk))
            cust))
  (define-values [a-thd a-cust]
    (spawn a-thunk))
  (define-values [b-thd b-cust]
    (spawn b-thunk))
  (define ready-thd
    (sync a-thd b-thd))
  (cond
    [(thread-wait ready-thd)
     => (λ (rslt)
          (custodian-shutdown-all (if (equal? ready-thd a-thd)
                                      b-cust
                                      a-cust))
          rslt)]
    [else
     (define-values [ready-cust other-thd other-cust]
       (if (equal? ready-thd a-thd)
           (values a-cust b-thd b-cust)
           (values b-cust a-thd a-cust)))
     (custodian-shutdown-all ready-cust)
     (cond
       [(thread-wait other-thd)]
       [else
        (custodian-shutdown-all other-cust)
        #f])]))

The dance with custodians is intended to arrange that a computation that either returns #f or is not chosen has all its resources cleaned up, but also to not shut down any resources from a computation that is chosen, since it might be useful to have parallel-or return a custodian-managed resource.

This implementation doesn't consider potential exceptions coming from one (or both) of the parallel computations. You could configure the threads' initial exception handlers to communicate exceptions, or further enhancements to thread (and/or integrating the new features into delay/thread) might make things more convenient:

2 Likes

That looks great! Is there a good way of managing multiple racket installations? I'd like to try out the new feature but would still like to have an easy way of reverting back to the stable release for other stuff.

The use case I have in mind is purely functional. I'm not familiar with custodians and I wonder how much the code could be simplified if both thunks are pure.

To provide some context, I was doing a coding exercise which involves a matrix of letters and the goal is to check whether you can find a string. I coded up a purely functional solution which ended up twice as slow as an imperative solution. I want to try parallelizing the algorithm to see if the functional data structures pay off with more cores. Parallel-or seems to make sense in this case because the short circuiting could give a performance boost.

There are several good options.

From a download site like https://pre-release.racket-lang.org, you can have the installer perform an in-place installation (or select a distribution packaged as a tarball), putting the whole installation in a directory like ~/racket-8.18.0.19/, so that the racket executable is at ~/racket-8.18.0.19/bin/racket. That works especially well for a one-off short-term use like this, given that the Racket 9.0 release should be on Friday, give or take.

The raco cross tool can serve as a more automated way of doing basically the same thing, even if you don't need its additional features for cross-compilation and such.

You can also install multiple Racket versions on a machine long-term, similar to the way /lib/qt5/ and /lib/qt6/ both exist on my laptop. Details will vary depending on your system. This is less likely to be useful for Racket, though, because Racket's strong emphasis on compatibility means you should almost never have to change your code to upgrade to the latest Racket release.

You can simplify somewhat (see below), but you need to substitute kill-thread for one of the custodian-shutdown-all calls, because, if one thread terminates with a non-false value, you surely want to terminate the other thread.

Custodians can manage more general resource-cleanup issues. For example, imagine you wanted to look up the weather in Chicago, and wanted to use parallel-or to try both the National Weather Service API and Open-Meteo, returning the result from whichever supplies it fastest. Using thread-kill would stop the thread, freeing CPU resources, but it could potentially leave open file descriptors or other OS-level resources implementing an HTTP connection. More generally, a given thread could have launched other threads to perform its work. Using custodians can reliably reclaim all potentially-used resources.

(define (do-parallel-or a-thunk b-thunk)
  (define (spawn thunk)
    (thread #:pool 'own #:keep 'results thunk))
  (define a-thd
    (spawn a-thunk))
  (define b-thd
    (spawn b-thunk))
  (define ready-thd
    (sync a-thd b-thd))
  (define other-thd
    (if (equal? ready-thd a-thd)
        b-thd
        a-thd))
  (cond
    [(thread-wait ready-thd)
     => (λ (rslt)
          (kill-thread other-thd)
          rslt)]
    [else
     (thread-wait other-thd)]))

Outside of an exercise, these find-a-string problems are very well studied, so you probably would want to make sure you're implementing one of the current state-of-the-art algorithms (though I couldn't say off-hand what you want). But your results from adding parallelism, whether to an optimal algorithm or not, might be useful to help evaluate the still-quite-new implementation of parallel threads:

1 Like