Skip to content

event loop

Chris Vine edited this page Jul 13, 2020 · 2 revisions

The (a-sync event-loop) module provides an event loop for asynchronous operations. The event loop does not of itself start any new threads. Instead it runs in the thread which calls event-loop-run!: that procedure will block while there are events to be processed. In most programs the event loop will run in the main program thread, with which the program began. Many programs using event loops are entirely single threaded.

However, the event loop provided by this module does support multiple threads. Most of the event loop procedures in this module are thread safe and may be called in any thread. The thread safe procedures are event-loop-run! (as mentioned, this will cause the event loop to run in the thread which calls it), event-loop-block!, event-loop-quit!, event-post!, event-loop-tasks, event-loop-add-read-watch!, event-loop-add-write-watch!, event-loop-remove-read-watch!, event-loop-remove-write-watch!, timeout-post! and timeout-remove!. In particular, event-post! allows worker threads to post an event to an event loop, say with the result of a computation, so that the event callback will execute in the event loop thread.

(a-sync meeting) provides synchronization facilities for event loops constructed under this module.

The file watches provided by this module use guile's wrapper for the select() system call. This has two consequences. First, in linux select() applied to sockets can give rise to spurious wake-ups, which means that it may be best with sockets to set the socket as non-blocking using the guile wrapper for the fcntl() system call in order to prevent a blocking read where it is not expected. With guile-3.0 this means that with such sockets, suspendable ports should be used with the await-read-suspendable! or await-write-suspendable! procedures in the (a-sync await-ports) module. Secondly, with select() the file descriptor value of a port passed to the watch should not exceed FD_SETSIZE. Normally, by default the maximum soft user limit for open files is the same as FD_SETSIZE, and it is best if this situation is retained by user code.

Where a file watch on a file descriptor or port has been added with event-loop-add-read-watch! or event-loop-add-write-watch!, the event loop should be treated as owning the descriptor or port until it has subsequently been removed with event-loop-remove-read-watch! or event-loop-remove-write-watch!. In particular, if the watch is on a port, an error in the fileno or select procedures will arise at some point if the port is closed (say by calling close, close-port, close-input-port or close-output-port) before its watch has been removed from the event loop; if the watch is on a file descriptor and the descriptor value is reused by the operating system, there may be a phantom watch on the new descriptor. Also note that, where a watch on a port has just been removed, it is usually undesirable to close the port in a different thread than the event loop thread because there is no easy way of synchronizing the threads at that point. If it is necessary for a worker thread to close a port after removing the watch, one approach is for the thread to post the closing as an event using event-post!.

Including this module will automatically enable suspendable ports. The uninstall-suspendable-ports! procedure should not subsequently be applied, or the procedures in this module (and in the (a-sync await-ports) module) will not work correctly.

This module provides the following procedures:


(set-default-event-loop! [loop])

The 'loop' (event loop) argument is optional. This procedure sets the default event loop for the procedures in this module to the one passed in (which must have been constructed by the make-event-loop procedure), or if no argument is passed (or #f is passed), a new event loop will be constructed for you as the default, which can be accessed via the get-default-event-loop procedure. The default loop variable is not a fluid or a parameter - it is intended that the default event loop is the same for every thread in the program, and that the default event loop would normally run in the thread with which the program started. This procedure is not thread safe - if it might be called by a different thread from others which might access the default event loop, then external synchronization may be required. However, that should not normally be an issue. The normal course would be to call this procedure once only on program start up, before other threads have started. It is usually a mistake to call this procedure twice: if there are asynchronous events pending (that is, if event-loop-run! has not returned) you will probably not get the results you expect.

Note that if a default event-loop is constructed for you because no argument is passed (or #f is passed), no throttling arguments are applied to it (see the documentation on make-event-loop for more about that). If throttling is wanted, the make-event-loop procedure should be called explicitly and the result passed to this procedure.


(get-default-event-loop)

This returns the default loop set by the set-default-event-loop! procedure, or #f if none has been set.


(make-event-loop [throttle-threshold throttle-delay])

This procedure constructs a new event loop object.

This procedure optionally takes two throttling arguments for backpressure when applying the event-post! procedure to the event loop. The 'throttle-threshold' argument specifies the number of unexecuted tasks queued for execution, by virtue of calls to event-post!, at which throttling will first be applied. Where the threshold is exceeded, throttling proceeds by adding a wait to any thread which calls the event-post! procedure, equal to the cube of the number of times (if any) by which the number of queued tasks exceeds the threshold multiplied by the value of 'throttle-delay'. The value of 'throttle-delay' should be given in microseconds. Throttling is only applied where the call to event-post! is made in a thread other than the one in which the event loop runs.

So if the threshold given is 10000 tasks and the delay given is 1000 microseconds, upon 10000 unexecuted tasks accumulating a delay of 1000 microseconds will be applied to callers of event-post! which are not in the event loop thread, at 20000 unexecuted tasks a delay of 8000 microseconds will be applied, and at 30000 unexecuted tasks a delay of 27000 microseconds will be applied, and so on.

If throttle-threshold and throttle-delay arguments are not provided (or #f is passed for them), then no throttling takes place.


(event-loop? obj)

This procedure indicates whether 'obj' is an event-loop object constructed by make-event-loop.


(event-loop-run! [loop])

The 'loop' (event loop) argument is optional. This procedure starts the event loop passed in as an argument, or if none is passed (or #f is passed) it starts the default event loop. The event loop will run in the thread which calls this procedure. If this procedure has returned, including after a call to event-loop-quit!, this procedure may be called again to restart the event loop, provided event-loop-close! has not been applied to the loop. If event-loop-close! has previously been invoked, this procedure will throw an 'event-loop-error exception. This procedure will also throw an 'event-loop-error exception if it is applied to an event loop which is currently running.

If something else throws in the implementation or a callback throws, then this procedure will clean up the event loop as if event-loop-quit! had been called, and the exception will be rethrown out of this procedure. This means that if there are continuable exceptions, they will be converted into non-continuable ones (but continuable exceptions are usually incompatible with asynchronous event handlers and may break resource management using rethrows or dynamic winds).


(event-loop-add-read-watch! file proc [loop])

The 'loop' (event loop) argument is optional. This procedure will start a read watch in the event loop passed in as an argument, or if none is passed (or #f is passed), in the default event loop. The 'proc' callback should take a single argument, and when called this will be set to 'in or 'excpt. The same port or file descriptor can also be passed to event-loop-add-write-watch, and if so and the descriptor is also available for writing, the write callback will also be called with its argument set to 'out. If there is already a read watch for the file passed, the old one will be replaced by the new one. If 'proc' returns #f, the read watch will be removed from the event loop, otherwise the watch will continue. This is thread safe - any thread may add a watch, and the callback will execute in the event loop thread. The file argument can be either a port or a file descriptor. If 'file' is a file descriptor, any port for the descriptor is not referenced for garbage collection purposes - it must remain valid while operations are carried out on the descriptor. If 'file' is a buffered port, buffering will be taken into account in indicating whether a read can be made without blocking (but on a buffered port, for efficiency purposes each read operation in response to this watch should usually exhaust the buffer by calling drain-input or by looping on char-ready?, or the port's ordinary input procedures should be used with suspendable ports using the await-read-suspendable! procedure in the (a-sync await-ports) module).

This procedure should not throw an exception unless memory is exhausted.


(event-loop-add-write-watch! file proc [loop])

The 'loop' (event loop) argument is optional. This procedure will start a write watch in the event loop passed in as an argument, or if none is passed (or #f is passed), in the default event loop. The 'proc' callback should take a single argument, and when called this will be set to 'out or 'excpt. The same port or file descriptor can also be passed to event-loop-add-read-watch, and if so and the descriptor is also available for reading or in exceptional condition, the read callback will also be called with its argument set to 'in or 'excpt (if both a read and a write watch have been set for the same file argument, and there is an exceptional condition, it is the read watch procedure which will be called with 'excpt rather than the write watch procedure, so if that procedure returns #f only the read watch will be removed). If there is already a write watch for the file passed, the old one will be replaced by the new one. If 'proc' returns #f, the write watch will be removed from the event loop, otherwise the watch will continue. This is thread safe - any thread may add a watch, and the callback will execute in the event loop thread. The file argument can be either a port or a file descriptor. If 'file' is a file descriptor, any port for the descriptor is not referenced for garbage collection purposes - it must remain valid while operations are carried out on the descriptor.

If 'file' is a buffered port, buffering will be taken into account in indicating whether a write can be made without blocking, either because there is room in the buffer for a character, or because the underlying file descriptor is ready for a character. This can have unintended consequences: if the buffer is full but the underlying file descriptor is ready for a character, the next write will cause a buffer flush, and if the size of the buffer is greater than the number of characters that the file can receive without blocking, blocking might still occur. Unless the port will carry out a partial flush in such a case, this procedure will therefore generally work best either with unbuffered ports (say by using the open-file, fdopen or duplicate-port procedure with the '0' mode option or the R6RS open-file-input-port procedure with a buffer-mode of none, or by calling setvbuf), or the port's ordinary output procedures should be used with suspendable ports using the await-write-suspendable! procedure in the (a-sync await-ports) module.

This procedure should not throw an exception unless memory is exhausted.


(event-loop-remove-read-watch! file [loop])

The 'loop' (event loop) argument is optional. This procedure will remove a read watch from the event loop passed in as an argument, or if none is passed (or #f is passed), from the default event loop. The file argument may be a port or a file descriptor. This is thread safe - any thread may remove a watch. A file descriptor and a port with the same underlying file descriptor compare equal for the purposes of removal.

This procedure should not throw an exception unless memory is exhausted.


(event-loop-remove-write-watch! file [loop])

The 'loop' (event loop) argument is optional. This procedure will remove a write watch from the event loop passed in as an argument, or if none is passed (or #f is passed), from the default event loop. The file argument may be a port or a file descriptor. This is thread safe - any thread may remove a watch. A file descriptor and a port with the same underlying file descriptor compare equal for the purposes of removal.

This procedure should not throw an exception unless memory is exhausted.


(event-post! action [loop])

The 'loop' (event loop) argument is optional. This procedure will post a callback for execution in the event loop passed in as an argument, or if none is passed (or #f is passed), in the default event loop. The 'action' callback is a thunk. This is thread safe - any thread may post an event (that is its main purpose), and the action callback will execute in the event loop thread. Actions execute in the order in which they were posted. If an event is posted from a worker thread, it will normally be necessary to call event-loop-block! beforehand.

This procedure should not throw an exception unless memory is exhausted. If the 'action' callback throws, and the exception is not caught locally, it will propagate out of event-loop-run!.

Where this procedure is called by other than the event loop thread, throttling may take place if the number of posted callbacks waiting to execute exceeds the threshold set for the event loop - see the documentation on make-event-loop for further details.


(timeout-post! msecs action [loop])

The 'loop' (event loop) argument is optional. This procedure adds a timeout to the event loop passed in as an argument, or if none is passed (or #f is passed), to the default event loop. The timeout will repeat unless and until the passed-in callback returns #f or timeout-remove! is called. The passed-in callback must be a thunk. This procedure returns a tag symbol to which timeout-remove! can be applied. It may be called by any thread, and the timeout callback will execute in the event loop thread.

This procedure should not throw an exception unless memory is exhausted. If the 'action' callback throws, and the exception is not caught locally, it will propagate out of event-loop-run!.


(timeout-remove! tag [loop])

The 'loop' (event loop) argument is optional. This procedure stops the timeout with the given tag from executing in the event loop passed in as an argument, or if none is passed (or #f is passed), in the default event loop. It may be called by any thread.

This procedure should not throw an exception unless memory is exhausted.


(event-loop-tasks [loop])

This procedure returns the number of callbacks posted to an event loop with the event-post! procedure which at the time still remain queued for execution. Amongst other things, it can be used by a calling thread which is not the event loop thread to determine whether throttling is likely to be applied to it when calling event-post! - see the documentation on make-event-loop for further details.

The 'loop' (event loop) argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed), on the default event loop. This procedure is thread safe - any thread may call it.


(event-loop-block! val [loop])

By default, upon there being no more watches, timeouts and posted events for an event loop, event-loop-run! will return, which is normally what you want with a single threaded program. However, this is undesirable where a worker thread is intended to post an event to the main loop after it has reached a result, say via await-task-in-thread!, because the main loop may have ended before it posts. Passing #t to the val argument of this procedure will prevent that from happening, so that the event loop can only be ended by calling event-loop-quit!, or by calling event-loop-block! again with a #f argument (to switch the event loop back to non-blocking mode, pass #f). This is thread safe - any thread may call this procedure. The 'loop' (event loop) argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed as the 'loop' argument), on the default event loop.

This procedure should not throw an exception unless memory is exhausted.


(event-loop-quit! [loop])

This procedure causes an event loop to end and event-loop-run! to return. Any file watches, timeouts or posted events remaining in the event loop will be discarded. New file watches, timeouts and events may subsequently be added or posted after event-loop-run! has returned, and event-loop-run! then called for them. This is thread safe - any thread may call this procedure, including any callback or task running on the event loop. The 'loop' argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed), on the default event loop. Applying this procedure to an event loop does not change the blocking status of the loop as may previously have been set by event-loop-block!, should event-loop-run! be applied to it again.

Applying this procedure to an event loop has no effect if the event loop is not actually running. Unlike the await-* procedures in this module, the await-* procedures in the (a-sync await-ports) module do not pass control to the event loop if they can operate immediately without waiting, so if (i) the event loop concerned has been set blocking by event-loop-block!, (ii) this procedure is invoked in an a-sync or compose-a-sync block in order to bring the event loop to an end, and (iii) before invoking this procedure the a-sync or compose-a-sync block has done nothing except make a call to one or more of the await-* procedures in the (a-sync await-ports) module, then in order to make sure the loop is running consider calling await-yield! or some similar procedure before applying this procedure.

Note that the discarding of file watches, timeouts and unexecuted events remaining in the event loop means that if one of the helper await-* procedures provided by this library has been called but has not yet returned, it may fail to complete, as its continuation may disappear - it will be as if the a-sync or compose-a-sync block concerned had come to an end. It may therefore be best only to call this procedure on an event loop after all await-* procedures which are executing in it have returned.

This procedure should not throw an exception unless memory is exhausted.


(event-loop-close! [loop])

This procedure closes an event loop. Like event-loop-quit!, if the loop is still running it causes the event loop to unblock, and any file watches, timeouts or posted events remaining in the event loop will be discarded. However, unlike event-loop-quit!, it also closes the internal event pipe ports, and any subsequent application of event-loop-run! to the event loop will cause an 'event-loop-error exception to be thrown.

You might want to call this procedure to ensure that, after an event loop in a local scope has been finished with, the two internal event pipe file descriptors used by the loop are released to the operating system in advance of the garbage collector releasing them when the event loop object becomes inaccessible.

This is thread safe - any thread may call this procedure, including any callback or task running on the event loop. The 'loop' argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed), on the default event loop.

Note that the discarding of file watches, timeouts and unexecuted events remaining in the event loop means that if one of the helper await-* procedures provided by this library has been called but has not yet returned, it may fail to complete, as its continuation may disappear - it will be as if the a-sync or compose-a-sync block concerned had come to an end. It may therefore be best only to call this procedure on an event loop after all such await-* procedures which are executing have returned.

This procedure should not throw an exception unless memory is exhausted.


(await-task-in-thread! await resume [loop] thunk [handler])

The loop and handler arguments are optional. The procedure will run 'thunk' in its own thread, and then post an event to the event loop specified by the 'loop' argument when 'thunk' has finished, or to the default event loop if no 'loop' argument is provided or if #f is provided as the 'loop' argument (pattern matching is used to detect the type of the third argument). This procedure calls 'await' and will return the thunk's return value. It is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It will normally be necessary to call event-loop-block! before invoking this procedure. If the optional 'handler' argument is provided, then that handler will be run in the event loop thread if 'thunk' throws and the return value of the handler would become the return value of this procedure; otherwise the program will terminate if an unhandled exception propagates out of 'thunk'. 'handler' should take a single argument, which will be the thrown exception object.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs, where the result of calling 'thunk' will be received. As mentioned above, the thunk itself will run in its own thread.

As the worker thread calls event-post!, it might be subject to throttling by the event loop concerned. See the documentation on the make-event-loop procedure for further information about that.

Exceptions may propagate out of this procedure if they arise while setting up (that is, before the worker thread starts), which shouldn't happen unless memory is exhausted or pthread has run out of resources. Exceptions arising during execution of the task, if not caught by a handler procedure, will terminate the program. Exceptions thrown by the handler procedure will propagate out of event-loop-run!.

Here is an example of the use of await-task-in-thread!:

(set-default-event-loop!) ;; if none has yet been set
(event-loop-block! #t) ;; because the task runs in another thread
(a-sync (lambda (await resume)
          (simple-format #t "1 + 1 is ~A\n"
                         (await-task-in-thread! await resume
                                                (lambda ()
                                                  (+ 1 1))))
          (event-loop-block! #f)))
(event-loop-run!)

(await-task-in-event-loop! await resume [waiter] worker thunk)

The 'waiter' argument is optional. The 'worker' argument is an event loop running in a different thread than the one in which this procedure is called, and is the one in which 'thunk' will be executed by posting an event to that loop. The result of executing 'thunk' will then be posted to the event loop specified by the 'waiter' argument, or to the default event loop if no 'waiter' argument is provided or if #f is provided as the 'waiter' argument, and will comprise this procedure's return value. This procedure is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It will normally be necessary to call event-loop-block! on 'waiter' (or on the default event loop) before invoking this procedure.

This procedure calls 'await' and must (like the a-sync procedure) be called in the same thread as that in which the 'waiter' or default event loop runs (as the case may be).

This procedure acts as a form of channel through which two different event loops may communicate. It also offers a means by which a master event loop (the waiter or default event loop) may allocate work to worker event loops for execution.

Depending on the circumstances, it may be desirable to provide throttling arguments when constructing the 'worker' event loop, in order to enable backpressure to be supplied if the 'worker' event loop becomes overloaded: see the documentation on the make-event-loop procedure for further information about that. (This procedure calls event-post! in both the 'waiter' and 'worker' event loops by the respective threads of the other, so either could be subject to throttling.)

Exceptions may propagate out of this procedure if they arise while setting up, which shouldn't happen unless memory is exhausted or pthread has run out of resources. Exceptions arising during execution of the task, if not caught locally, will propagate out of the event-loop-run! procedure called for the 'worker' event loop.

Here is an example of the use of await-task-in-event-loop!:

(set-default-event-loop!)     ;; if none has yet been set
(define worker (make-event-loop))
(event-loop-block! #t)        ;; because the task runs in another thread
(event-loop-block! #t worker)

(call-with-new-thread
 (lambda ()
   (event-loop-run! worker)))

(a-sync (lambda (await resume)
	  (let ((res
		 (await-task-in-event-loop! await resume worker
					    (lambda ()
					      (+ 5 10)))))
	    (simple-format #t "~A\n" res)
	    (event-loop-block! #f worker)
	    (event-loop-block! #f))))
(event-loop-run!)

(await-task! await resume [loop] thunk)

The 'loop' argument is optional. This is a convenience procedure for use with an event loop, which will run 'thunk' in the event loop specified by the 'loop' argument, or in the default event loop if no 'loop' argument is provided or #f is provided as the 'loop' argument. This procedure calls 'await' and will return the thunk's return value. It is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It is the single-threaded corollary of await-task-in-thread!. This means that (unlike with await-task-in-thread!) while 'thunk' is running other events in the event loop will not make progress, so blocking calls should not be made in 'thunk'.

This procedure can be used for the purpose of implementing co-operative multi-tasking. However, when 'thunk' is executed, this procedure is waiting on 'await', so 'await' and 'resume' cannot be used again in 'thunk' (although 'thunk' can call a-sync to start another series of asynchronous operations with a new await-resume pair). For that reason, await-yield! is usually more convenient for composing asynchronous tasks. In retrospect, this procedure offers little over await-yield!, apart from symmetry with await-task-in-thread!.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs.

This procedure calls event-post! in the event loop concerned. This is done in the same thread as that in which the event loop runs so it cannot of itself be throttled. However it may contribute to the number of accumulated unexecuted tasks in the event loop and therefore contribute to the throttling of other threads by the loop. See the documentation on the make-event-loop procedure for further information about that.

Exceptions may propagate out of this procedure if they arise while setting up (that is, before the task starts), which shouldn't happen unless memory is exhausted. Exceptions arising during execution of the task, if not caught locally, will propagate out of event-loop-run!.

Here is an example of the use of await-task!:

(set-default-event-loop!) ;; if none has yet been set
(a-sync (lambda (await resume)
          (simple-format #t "1 + 1 is ~A\n"
                         (await-task! await resume
                                      (lambda ()
                                        (+ 1 1))))))
(event-loop-run!)

(await-yield! await resume [loop])

This is a convenience procedure which will surrender execution to the relevant event loop, so that code in other a-sync or compose-a-sync blocks can run. The remainder of the code after the call to await-yield! in the current a-sync or compose-a-sync block will execute on the next iteration through the loop. It is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It's effect is similar to calling await-task! with a task that does nothing.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the relevant event loop runs: for this purpose "the relevant event loop" is the event loop given by the 'loop' argument, or if no 'loop' argument is provided or #f is provided as the 'loop' argument, then the default event loop.

This procedure calls event-post! in the event loop concerned. This is done in the same thread as that in which the event loop runs so it cannot of itself be throttled. However it may contribute to the number of accumulated unexecuted tasks in the event loop and therefore contribute to the throttling of other threads by the loop. See the documentation on the make-event-loop procedure for further information about that.

This procedure should not throw any exceptions unless memory is exhausted.

Here is an example of the use of await-yield!:

(set-default-event-loop!) ;; if none has yet been set
(a-sync (lambda (await resume)
          (display "In first iteration through event loop\n")
          (await-yield! await resume)
          (display "In next iteration through event loop\n")))
(event-loop-run!)

(await-generator-in-thread! await resume [loop] generator proc [handler])

The 'loop' and 'handler' arguments are optional. The 'generator' argument is a procedure taking one argument, namely a yield argument (see the documentation on the make-iterator procedure for further details). This await-generator-in-thread! procedure will run 'generator' in its own worker thread, and whenever 'generator' yields a value will cause 'proc' to execute in the event loop specified by the 'loop' argument (or in the default event loop if no 'loop' argument is provided or if #f is provided as the 'loop' argument - pattern matching is used to detect the type of the third argument).

'proc' should be a procedure taking a single argument, namely the value yielded by the generator. If the optional 'handler' argument is provided, then that handler will be run in the event loop thread if 'generator' throws; otherwise the program will terminate if an unhandled exception propagates out of 'generator'. 'handler' should take a single argument, which will be the thrown exception object.

This procedure calls 'await' and will return when the generator has finished or, if 'handler' is provided, upon the generator throwing an exception. This procedure will return #f if the generator completes normally, or 'guile-a-sync-thread-error if the generator throws an exception and 'handler' is run (the 'guile-a-sync-thread-error symbol is reserved to the implementation and should not be yielded by the generator).

This procedure is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It will normally be necessary to call event-loop-block! before invoking this procedure.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs. As mentioned above, the generator itself will run in its own thread.

As the worker thread calls event-post!, it might be subject to throttling by the event loop concerned. See the documentation on the make-event-loop procedure for further information about that.

Exceptions may propagate out of this procedure if they arise while setting up (that is, before the worker thread starts), which shouldn't happen unless memory is exhausted or pthread has run out of resources. Exceptions arising during execution of the generator, if not caught by a handler procedure, will terminate the program. Exceptions thrown by the handler procedure will propagate out of event-loop-run!. Exceptions thrown by 'proc', if not caught locally, will also propagate out of event-loop-run!.

Here is an example of the use of await-generator-in-thread!:

(set-default-event-loop!) ;; if none has yet been set
(event-loop-block! #t) ;; because the generator runs in another thread
(a-sync (lambda (await resume)
          (await-generator-in-thread! await resume
                                      (lambda (yield)
                                        (let loop ((count 0))
                                          (when (< count 5)
                                            (yield (* 2 count))
                                            (loop (1+ count)))))
                                      (lambda (val)
                                        (display val)
                                        (newline)))
          (event-loop-block! #f)))
(event-loop-run!)

(await-generator-in-event-loop! await resume [waiter] worker generator proc)

The 'waiter' argument is optional. The 'worker' argument is an event loop running in a different thread than the one in which this procedure is called. The 'generator' argument is a procedure taking one argument, namely a yield argument (see the documentation on the make-iterator procedure for further details). This await-generator-in-event-loop! procedure will cause 'generator' to run in the 'worker' event loop, and whenever 'generator' yields a value this will cause 'proc' to execute in the event loop specified by the 'waiter' argument, or in the default event loop if no 'waiter' argument is provided or if #f is provided as the 'waiter' argument. 'proc' should be a procedure taking a single argument, namely the value yielded by the generator.

This procedure is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It will normally be necessary to call event-loop-block! on 'waiter' (or on the default event loop) before invoking this procedure.

This procedure calls 'await' and will return when the generator has finished. It must (like the a-sync procedure) be called in the same thread as that in which the 'waiter' or default event loop runs (as the case may be).

This procedure acts, with await-task-in-event-loop!, as a form of channel through which two different event loops may communicate. It also offers a means by which a master event loop (the waiter or default event loop) may allocate work to worker event loops for execution.

Depending on the circumstances, it may be desirable to provide throttling arguments when constructing the 'worker' event loop, in order to enable backpressure to be supplied if the 'worker' event loop becomes overloaded: see the documentation on the make-event-loop procedure for further information about that. (This procedure calls event-post! in both the 'waiter' and 'worker' event loops by the respective threads of the other, so either could be subject to throttling.)

Exceptions may propagate out of this procedure if they arise while setting up, which shouldn't happen unless memory is exhausted or pthread has run out of resources. Exceptions arising during execution of the generator, if not caught locally, will propagate out of the event-loop-run! procedure called for the 'worker' event loop. Exceptions arising during the execution of 'proc', if not caught locally, will propagate out of the event-loop-run! procedure called for the 'waiter' or default event loop, as the case may be.

Here is an example of the use of await-generator-in-event-loop!:

(set-default-event-loop!)     ;; if none has yet been set
(define worker (make-event-loop))
(event-loop-block! #t)        ;; because the generator runs in another thread
(event-loop-block! #t worker)

(call-with-new-thread
 (lambda ()
   (event-loop-run! worker)))

(a-sync (lambda (await resume)
          (await-generator-in-event-loop! await resume worker
                                          (lambda (yield)
                                            (let loop ((count 0))
                                              (when (< count 5)
                                                (yield (* 2 count))
                                                (loop (1+ count)))))
                                          (lambda (val)
                                            (display val)
                                            (newline)))
          (event-loop-block! #f worker)
          (event-loop-block! #f)))
(event-loop-run!)

(await-generator! await resume [loop] generator proc)

The 'loop' argument is optional. The 'generator' argument is a procedure taking one argument, namely a yield argument (see the documentation on the make-iterator procedure for further details). This await-generator! procedure will run 'generator', and whenever 'generator' yields a value will cause 'proc' to execute in the event loop specified by the 'loop' argument, or in the default event loop if no 'loop' argument is provided or #f is provided as the 'loop' argument. 'proc' should be a procedure taking a single argument, namely the value yielded by the generator. Each time 'proc' runs it will do so as a separate event in the event loop and so be multi-plexed with other events.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs.

This procedure is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). It is the single-threaded corollary of await-generator-in-thread!. This means that (unlike with await-generator-in-thread!) while 'generator' is running other events in the event loop will not make progress, so blocking calls (other than to the yield procedure) should not be made in 'generator'. This procedure can be useful for the purpose of implementing co-operative multi-tasking, say by composing tasks with compose-a-sync (see compose.scm).

When 'proc' executes, 'await' and 'resume' will still be in use by this procedure, so they may not be reused by 'proc' (even though 'proc' runs in the event loop thread).

This procedure calls event-post! in the event loop concerned. This is done in the same thread as that in which the event loop runs so it cannot of itself be throttled. However it may contribute to the number of accumulated unexecuted tasks in the event loop and therefore contribute to the throttling of other threads by the loop. See the documentation on the make-event-loop procedure for further information about that.

Exceptions may propagate out of this procedure if they arise while setting up (that is, before the task starts), which shouldn't happen unless memory is exhausted. Exceptions arising during execution of the generator, if not caught locally, will propagate out of event-loop-run!. Exceptions thrown by 'proc', if not caught locally, will propagate out of event-loop-run!.

Here is an example of the use of await-generator!:

(set-default-event-loop!) ;; if none has yet been set
(a-sync (lambda (await resume)
          (await-generator! await resume
                            (lambda (yield)
                              (let loop ((count 0))
                                (when (< count 5)
                                  (yield (* 2 count))
                                  (loop (1+ count)))))
                            (lambda (val)
                              (display val)
                              (newline)))))
(event-loop-run!)

(await-timeout! await resume [loop] msecs thunk)

This is a convenience procedure for use with an event loop, which will run 'thunk' in the event loop thread when the timeout expires. This procedure calls 'await' and will return the thunk's return value. It is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). The timeout is single shot only - as soon as 'thunk' has run once and completed, the timeout will be removed from the event loop. The 'loop' argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed), on the default event loop.

In practice, calling await-sleep! may often be more convenient for composing asynchronous code than using this procedure. That is because, when 'thunk' is executed, this procedure is waiting on 'await', so 'await' and 'resume' cannot be used again in 'thunk' (although 'thunk' can call a-sync to start another series of asynchronous operations with a new await-resume pair). In retrospect, this procedure offers little over await-sleep!.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs.

Exceptions may propagate out of this procedure if they arise while setting up (that is, before the first call to 'await' is made), which shouldn't happen unless memory is exhausted. Exceptions thrown by 'thunk', if not caught locally, will propagate out of event-loop-run!.

Here is an example of the use of event-timeout!:

(set-default-event-loop!) ;; if none has yet been set
(a-sync (lambda (await resume)
          (simple-format #t
                         "Timeout ~A\n"
                         (await-timeout! await resume
                                         100
                                         (lambda ()
                                           "expired")))))
(event-loop-run!)

(await-sleep! await resume [loop] msecs)

This is a convenience procedure which will suspend execution of code in the current a-sync or compose-a-sync block for the duration of 'msecs' milliseconds. The event loop will not be blocked by the sleep - instead any other events in the event loop (including any other a-sync or compose-a-sync blocks) will be serviced. It is intended to be called within a waitable procedure invoked by a-sync (which supplies the 'await' and 'resume' arguments). The 'loop' argument is optional: this procedure operates on the event loop passed in as an argument, or if none is passed (or #f is passed), on the default event loop.

Calling this procedure is equivalent to calling await-timeout! with a 'proc' argument comprising a lambda expression that does nothing.

This procedure must (like the a-sync procedure) be called in the same thread as that in which the event loop runs.

This procedure should not throw any exceptions unless memory is exhausted.

Here is an example of the use of await-sleep!:

(set-default-event-loop!) ;; if none has yet been set
(a-sync (lambda (await resume)
	  (display "Entering sleep\n")
	  (await-sleep! await resume 500)
	  (display "Timeout expired\n")))
(event-loop-run!)

The (a-sync event-loop) module also loads the (a-sync monotonic-time) module.