2 ms·
You’re right. I think the other commenters aren’t being straightforward with you. It is a race condition to load a file one function at a time, because any othe
by sillysaurusx 4y ago
You’re right. I think the other commenters aren’t being straightforward with you. It is a race condition to load a file one function at a time, because any other thread can preempt you.
Arc has a particularly elegant solution to this. Any code you want to happen atomically, you wrap in (atomic …)
So
(atomic x y z)
Will do x, y, then z. During this time, no other threads are allowed to run.
Therefore your load-file function might look like (pseudo code because phone):
(def load (filename)
(atomic
(each form (read-file filename)
(eval form))))
Now any time you call (load “code.arc”), it’s guaranteed that none of the other threads will run “mid-update”. If your file contains definitions that overwrite all functions in your program, then your entire program is guaranteed to update atomically.
Under the hood, (atomic …) is implemented with a recursive lock (cf. Python’s RLock). That way, if you call a function that calls atomic, which calls another function that calls atomic, you won’t deadlock — it’s the same thread, and the same thread can always acquire the lock recursively.
And that’s it. The lock is literally a single instance of a recursive mutex, stored globally, created at program startup.
Astute readers will notice one pitfall: suppose there are 10 threads running, and then you load file, which replaces all of the functions those threads were running. What happens?
Each thread is paused in the middle of some existing function. That function will continue to exist until nothing refers to it. Since those threads refer to those functions (because we’re paused at some spot in the function), the currently-executing functions wont vanish until all the threads wake up and return.
… which is particularly problematic if your thread is a while true: “do this forever” loop! There’s no way to update it anymore. You’d have to kill the thread and restart.
Which is why the solution is “don’t do that, do this.” Get rid of the while loop, and call yourself recursively. Now whenever the new function loads, calling that function by name means you’ll jump into the new function, abandoning the old one.
In languages without tail recursion (lookin at you Python, bastard), you can still achieve this by making sure your thread runner is a while-true loop that just calls some other function, and nothing else.
- tmtvl 4y agoBut if your tail recursion gets compiled down into the standard "label whatever, code blablabla, set address register to label whatever, jump" type-code, how do you change the address that the register gets set to? Unless you do a lookup in the environment every time you recur to find where you have to jump to, but that would kill your performance stone dead.
- sillysaurusx 4y agoI suspect (but cannot prove) that environment lookups are cached, so that when the new definition is loaded with the same name as the previous jump target, the compiled code is invalidated and switches back to interpreted mode. But now that I talk through it, that sounds pretty hard. Occasionally I dive into racket’s good old C code for masochistic pleasure (it’s good code, just … very C, and very old). I always come away with a feeling of “well, that’s an interesting puzzle… I wonder what will happen when X happens” for every X that catches my attention. (Yesterday X was “How do racket’s thread-local parameters propagate their current values to new threads spawned by a subthread?) Still, I’m ~50% confident that my original explanation is right, even if the details are wrong. I would be shocked if a compiled tail-recursive function didn’t jump into its own new definition when a new definition is loaded for the previous jump target (the global function name). Test it out and check what happens empirically. :) Who knows. I’ve switched to Python long ago for daily tasks, since occasionally I enjoy “actually getting things done quickly, rather than endlessly researching interesting theoretical programming questions,” and Python is 60x slower than JavaScript (which doesn’t have tail recursion either), so #shruggyface. The world has apparently decided that tail recursion was an idea best left to the 90’s. EDIT: After thinking it over, I bet you’re right and my original point about tail recursion is mistaken. And indeed, I remember now that in Arc, all the threads tend to be while-loops that just call another global function to do its work. Probably for this exact reason. Very impressive callout. And a nice reminder to limit myself to talking about the things I actually do, not what I might theoretically do. At least not without qualifications and disclaimers. Thanks!
- tmtvl 4y agoThank you for calling it an impressive call out, but I actually am that idiot who didn't immediately think "oh yeah, you just cache the lookup and invalidate the cache when the thing gets changed." Still, it's a good thing you mentioned Racket's old C codebase, I'll have to dive into that some time to see if there's anything interesting to learn from it.