Dynamic binding is an evaluation rule for free variables. The rule is that the value of a free variable in a lambda expression is the value bound in the nearest dynamically enlosing scope. This is in contrast to static binding, where the value is the value bound in the nearest lexically enclosing scope. A naive interpreter that saves variable bindings in a stack frame will usually end up implementing dynamic binding.
In general, static binding is preferred because it is easier to reason about. However, dynamic binding is sometimes very useful in handling context. Lisp provides both static and dynamic binding, with the default being static binding.
Golang provides lexical binding, but not dynamic binding. But you can mimic the effect of dynamic binding by saving the variable's value, assigning it a new value, then restoring the old value in a defer statement. But this trick won't work in a multi-threaded environment. Each thread has to manage its own value of the variable that is separate from the values in other threads.
But what if the implementation doesn't tightly bind continuations to threads? What if the implementation uses a thread pool, and the same thread is used for different goroutines at different times? In that case, the trick of saving and restoring the value of a variable won't work. The value of the variable will be shared among different goroutines, and the value will be unpredictable. To make things work, your dynamic variables should be continuation-local.
But golang doesn't provide continuation-local variables, and it seems to go out of its way to keep code from inspecting the current continuation. There is a trick that you can use.
We need a way to allocate synthetic continuation IDs and bind them to the chain of pending calls. We also need a way that a subroutine can inspect the current chain of pending calls and determine the synthetic continuation ID.
Allocating a synthetic continuation ID is easy enough by maintaining a bitmap of free continuation IDs. When we need a continuation ID, we grab a mutex and search the bitmap for an unused continuation ID. We set the bit to allocate the ID and free the mutex. To free the continuation ID, we reset the bit. Any time you want to use dynamic variables, you need to allocate a synthetic continuation ID early on. This will be used as an index into a value table for each dynamic variable.
var ( continuationIdInUse [MaxContinuations]bool = [MaxContinuations]bool{} continuationIdMutex sync.Mutex = sync.Mutex{} ) func AllocateContinuationId[T any](receiver func(tid uint8) T) T { continuationIdMutex.Lock() for tid, inUse := range continuationIdInUse { if (tid > 0) && !inUse { continuationIdInUse[tid] = true continuationIdMutex.Unlock() return DeallocatingContinuationId(uint8(tid), receiver) } } panic("No available continuation ids") } func DeallocateContinuationId(tid uint8) { continuationIdInUse[tid] = false } func DeallocatingContinuationId[T any](tid uint8, receiver func(tid uint8) T) T { defer DeallocateContinuationId(tid) return receiver(tid) }
Golang doesn’t offer much in the way of stack introspection or manipulation, but you can easily get a backtrace of the names of the pending function calls. So we’ll encode the synthetic continuation ID as a nested set of function calls. When we need to read the current continuation ID, we get a backtrace of the stack and look for the special nested set of function calls and decode them.
func Mark0[T any](count int, value uint8, thunk func() T) T { if count == 0 { return thunk() } else if value%2 == 0 { return Mark0(count-1, value/2, thunk) } else { return Mark1(count-1, (value-1)/2, thunk) } } func Mark1[T any](count int, value uint8, thunk func() T) T { if count == 0 { return thunk() } else if value%2 == 0 { return Mark0(count-1, value/2, thunk) } else { return Mark1(count-1, (value-1)/2, thunk) } } func MarkByte[T any](value uint8, thunk func() T) T { if value%2 == 0 { return Mark0(7, value/2, thunk) } else { return Mark1(7, (value-1)/2, thunk) } }
When we call MarkByte
on a byte, we’ll make a series
of eight nested function calls to Mark0
or Mark1
depending on the bits in the byte. We can use
the golang backtrace mechanism to find the nested function calls and
determine what the byte was:
func ReadMark() int { pc := make([]uintptr, 256) n := runtime.Callers(0, pc) frames := runtime.CallersFrames(pc[:n]) value := 0 for { frame, more := frames.Next() if !more { break } if strings.Contains(frame.Function, "Mark0") { value *= 2 } else if strings.Contains(frame.Function, "Mark1") { value = value*2 + 1 } } return value }
Now when we spin off a goroutine that will use dynamic variables,
we must be careful to call WithThread
some time early
on in the goroutine:
func MakeMyGoroutineEntry () func () { return func () { _ = WithThread(func () int { // There are 8 pending calls to Mark[0,1] // And we can use ReadMark() to get the // represented value // ... body of callback ... }) } } // ... some code somewhere ... myGoroutine := MakeMyGoroutineEntry() go myGoroutine()
Now we can use the continuation ID to index into a table of values:
type GlVal[T any] struct { values [MaxThreads]T } func NewGlVal[T any]() *GlVal[T] { return &GlVal[T]{} }
We read the goroutine local variable by indexing into the table:
func GlValGet[T any](g *GlVal[T]) T { return g.values[ReadMark()] }
We write the golang local variable by writing to the table. We return the old value so that we can restore it later. Note, no mutex is needed because the value is thread-local:
func GlValSet[T any](g *GlVal[T], value T) T { mark := ReadMark() oldValue := g.values[mark] g.values[mark] = value return oldValue }
When we want to dynamically bind the variable, we save the old value to a frame local variable, set the new value, and defer a function to restore the old value:
func GlValBind[T, U any](g *GlVal[T], value T, thunk func() U) U { oldValue := GlValSet(g, value) defer GlValSet(g, oldValue) return thunk() }
This technique seems reasonably robust despite using the debugging support for its implementation. It will fail if they ever make golang tail-recursive, but I doubt that will happen any time soon.
No comments:
Post a Comment