Events and concurrency

When looking at the events reference page I saw that there are a number of procedures that are very similar. While the difference between wrap-evt and handle-evt is understandable and I can see the use cases for both I'm having a hard time finding use cases (and relative differences) between guard-evt, nack-guard-evt and poll-guard-evt.

I think I can implement cancellation using nack-guard-evt. As an example I can try and fetch a resource from different servers concurrently by spawning competing threads inside a nack-guard-evt.

(sync
    ;; spawn the first thread 
    (nack-guard-evt (lambda (nack)
                            (thread (lambda ()
                                            (sync
                                                 nack ;; we got cancelled, cleanup any resource we may have allocated
                                                 ...)))))
   ;; spawn the competition

Now the first thread that returns an event will be selected and the rest will receive a "negative acknowledgment" so they can stop doing any work.

In poll-guard-evt it's not clear what the difference between polling and blocking synchronization is. Is it related to sync and sync/timeout?

In guard-evt the explicit difference with wrap-evt and handle-evt is that the maker procedure can only be called at most once per sync call. I really have no idea how this can be used but I'm thinking it may be useful if the maker procedure is expensive.

Moreover in what cases can sync call an event generator procedure more than once? My understanding is that sync loops over the events I give it and waits for an event to be ready. This means each expression I put in my sync will be evaluated to an event but it's not clear now if each loop evals the expressions from scratch and more than one time.

2 Likes

Yes, nack-guard-evt is to implement request cancellation. There is an example in the paper "Kill-Safe Synchronization Abstractions".

It seems that a call to sync/timeout with a timeout of zero or a procedure is considered a "poll" and anything else is considered "blocking". Source: In /racket/src/thread/sync, the sync-poll function calls a poll-guard-evt's maker with the value of just-poll?; that argument is determined by the calls from do-sync.

I'm confused, because you seem to be talking about two unrelated things here. Can you clarify?

First, sync is a procedure, so its argument expressions are always evaluated eagerly and in order, just like for any other procedure. Some event values, like instances of guard-evt, represent explicitly delayed computation of the "real" event(s) that you want to synchronize on. These delayed events are forced in a pseudo-random order. I believe that when a delayed event is forced, it is replaced in the sync call's current list of candidates with the new event(s) that it produces, so it generally won't get forced multiple times. (Except if there are duplicates in the original list, or in strange cases like a guard-evt instance whose maker just returns the same guard-evt instance.)

4 Likes

I'm confused, because you seem to be talking about two unrelated things here. Can you clarify?

All the procedures *-evt procedures take an event as an argument except for guard-evt. If I have a event generating procedure my-evt I can also use it with all *-evt procedures but with guard-evt I don't have to call it right now. In (wrap-evt (my-evt) ...) my-evt is evaluated before sync and in (guard-evt my-evt) instead it is evaluated after sync has started the loop?

Still I'm confused by the words " The maker procedure may be called by sync at most once for a given call to sync". If I were to pass to sync the event (my-evt) the my-evt it is evaluated once and wrapping it in (guard-evt (my-evt)) may evaluate it at most once. When would I want to use this?

1 Like

The main purpose of guard-evt is to create an event that depends on the state when the sync call begins. For example, if sync/timeout were not available, you could implement something similar with the following:

;; sleep-evt : Real -> Evt
(define (sleep-evt seconds)
  (guard-evt
    (lambda ()
      (alarm-evt (+ (current-inexact-milliseconds) (* 1000.0 seconds))))))

;; sleep-10s-evt : Evt
(define sleep-10s-evt (sleep-evt 10))

Here, sleep-10s-evt can be defined once and used many times to implement a 10 second timeout. Because of the guard-evt wrapper, each time it is used in a sync call, it (potentially) creates a fresh primitive alarm-evt. If you deleted the guard-evt wrapper, it would time out 10 seconds from the definition of sleep-10s-evt, and after that time passed it would be equivalent to always-evt.

Another common pattern is to use guard-evt to read state from variables. In that case the variables should only be accessible by the thread syncing on the guard event; otherwise you can get races.

3 Likes

Thanks so much for all the help and the pointer to the "Kill-Safe Synchronization Abstractions" paper.