HL Legal Specifications: Global Variable Lock
The Common Layer includes a resource known as the Global Variable Lock, or GVL. This resource acts as a mutual exclusion lock that protects the global variables.
The Common Layer provides a pair of functions that are used to acquire and release this resource. The Common Layer also provides a utility higher-order function which executes another function while owning the lock. These functions are not portable across machines, and should always be referred to by name.
The Standard Layer includes a
macro form that calls the Common Layer GVL owning function.
The GVL is not fair. When there is high contention on the lock, it is possible for a single process to be unfairly blocked for an extended period of time, while other processes are able to acquire the lock and execute.
The GVL is a mutex lock provided by the Common Layer. Only one
process at a time can acquire or "have" the GVL; if a process
R attempts to acquire the GVL while another process
it, then process
R will be blocked until
O releases the
The purpose of the GVL is to support synchronization of access to the global variables. For this reason, acquiring and releasing the GVL automatically inserts global variable barriers, as described in the document Global Variables.
GVL Acquire and Release
Two functions in the Common Layer allow acquisition and release
of the GVL,
Both functions described here are non-portable across machines. Attempting to store these functions directly into a local variable will cause undefined behavior if a function or continuation containing that variable is transmitted to another virtual machine and that function is entered. Attempting to store these functions directly into some other structure, and transmitting that structure, as well as transmitting the function themselves, will cause undefined behavior if those structures are accessed or those functions are invoked. Wrapping functions that refer to the global variable containing these two functions should be used instead.
<common>acquire-gvl acquires the GVL. This function blocks
until the current process has acquired the GVL; upon returning,
the process already acquires the GVL, and will prevent other
processes from executing when those processes attempt to acquire
The function will automatically execute the special form
(<axiom>acquire) after acquiring the lock and just before
returning. It will then return
Calling this function when the current process already has the GVL will result in undefined behavior.
<common>release-gvl releases the GVL. This function does not
block, except in the case of undefined behavior (where the
behavior, obviously, is undefined). Upon returning, other
processses may subsequently continue if they were blocked while
attempting to acquire the lock.
The function will automatically execute the special form
(<axiom>release) before releasing the lock. It will then
Calling this function when the current process does not already have the GVL will result in undefined behavior.
The Common Layer also includes a
higher-order function that calls another function while owning
the lock. Acquisition and release of the lock is protected by
The given object
f must be of any callable type that accepts a
call without any parameters. The given object
f is called a
function from here onwards, but implementations should support
any callable type.
This function is not normally recontinuable - when any continuation outside this form has been activated, then calling a continuation inside this form will result in undefined behavior:
(let k (<common>call-w/gvl (fn () (ccc idfn))) (if k (k nil))) ; !!!!UNDEFINED BEHAVIOR!!!!
Calls to this function may be nested, i.e. the function called by this function may directly or indirectly call this function again. When this function is called while already in the scope of the GVL ownership, this function simply calls the given function without any additional protection.
(def (example) (<common>call-w/gvl (fn () (zap + global 1)))) (<common>call-w/gvl (fn () (zap + global 1) (example))) ; call-w/gvl in (example) will act like 'do
In order to determine if the current process already owns the
GVL, the implementation uses the
<common>gvl-acquired slot in
process-locals table provided by the Common Layer. This
slot is set to a true value when te current process already owns
Attempting to replace the process-locals table while within the execution context of this function will result in undefined behavior. For example, an implementation may retain a reference to the table before passing control to the given user function; if the given function subsequently replaces the entire process-locals table, the implementation and the user function will end up using different tables.
The Standard Layer also includes a macro
w/gvl. This macro form is a body-type macro, whose
sub-elements are individual expressions to be executed in order.
This macro form wraps the
(w/gvl ; will now have the GVL (zap + global 1) (if (err-condition) (err-throw t))) ; !! Will still release GVL
The macro expands to a protected call to
(macex '(w/gvl (foo) (bar))) => ((<axiom>symeval '<common>call-w/gvl) (<hl>fn nil (<User>foo) (<User>bar)))
Because this macro expands to a call to
inherits the warnings, capabilities and limitations of that
Definition Macros in Standard
The definitional macros in the Standard
Layer, such as
defcall automatically protect their definitions with the
In the cases of
defcall, the entire
read-modify-write is done within a
Since the GVL is an explicit lock, normal precautions to prevent deadlock should be explicitly applied by users of the lock. For example, it is possible to deadlock if you have acquired the lock, then wait for a message from another process. If the other processes that would send the message attempts to acquire the GVL before it can send the message, your process will never awaken.
Below is a reference implementation for the Common Layer
functions. It assumes that it is tied directly to the concrete
machine implementation, and thus uses the VM-implementation-
(in-package impl) (using <axiom>v1) ; to protect against undefined <axiom>recv behavior when ; it is not the only expression in the function. (set <common>recv (lambda () (recv))) (set <impl>gvl-p (new-process (lambda () ((rlambda acquired (owner pendings) (send owner (my-pid)) ((rlambda check-release (req pendings) (if (is owner req) (if pendings (acquired (car pendings) (cdr pendings)) (acquired (<common>recv) '())) (check-release (<common>recv) (cons req pendings)))) (<common>recv) pendings)) (<common>recv) '())))) (set <common>acquire-gvl (lambda () (send <impl>gvl-p (my-pid)) ((rlambda waiting (msg pending) (if (is msg <impl>gvl-p) ((rlambda resend (pending) (if pending ((lambda () (send (my-pid) (car pending)) (resend (cdr pending)))) (<axiom>acquire))) pending) (waiting (<common>recv) (cons msg pending)))) (<common>recv) '()))) (set <common>release-gvl (lambda () (<axiom>release) (send <impl>gvl-p (my-pid)) nil))
Below is a reference implementation of
(in-package impl) (using <axiom>v1) (set <common>call-w/gvl (lambda (bf) ((lambda (pl) (if (pl '<impl>w/gvl-f) (bf) ((lambda (first) (<common>dynamic-wind (fn () ; protect against re-entry (if first (set first nil) (error (tag '<hl>continue "attempt to recontinue 'w/gvl form"))) (sref pl t '<impl>w/gvl-f)) (<common>acquire-gvl)) bf (fn () (<common>release-gvl) (sref pl nil '<impl>w/gvl-f)))) t))) (process-locals))))