4 ms·
That sounds really fascinating and educational. Could you possibly provide some more details or examples of how you accomplished that in C?
by versteegen 11y ago
That sounds really fascinating and educational. Could you possibly provide some more details or examples of how you accomplished that in C?
- dllthomas 11y agoI'm a bit sleep deprived (new baby) but I'd like to try to respond before the reply link goes away. There's a few layers to this. I threaded knowledge of what thread a function could run on through as the first argument. It was always called `th` and took the form of an empty struct. It turns out, if you pass an empty struct by value and don't do anything but pass it along, modern C compilers seem to generate identical byte code as if you removed the argument entirely (obviously, this isn't something you could strictly rely on, but it made no difference for correctness only efficiency, and if you're at the level where you care about that scale of efficiency you should be verifying all manner of assumptions when moving between compilers!). So now at any given point in any given thread-pinned function, you've got a variable in scope called `th`. Calling another thread-pinned function on the same thread looks like `foo(th, other, args)`. If that function isn't supposed to run on this thread, you get an error about trying to pass something of type `server_thread_t` as an argument of type `main_thread_t` (or whatever). Controlling data access could be done with getters and setters. I took a slightly different approach (which I'm not convinced is better, but it didn't cause me any pain this project), which was to define a macro used to declare global data that was bound to a thread, which would mangle the name and also create a no-op checking function. I had another macro that would take the simple name, check the thread, and resolve to the thing with the mangled name (as an LVAL). Having called that macro `L`, I now can treat `L(foo)` just as if it were a global variable named `foo`, and at runtime that's all it is, but at compile time my access to it is checked. I had a few other macros for putting a "lying" `th` in scope for those places I really needed to cross threads (`CROSSTH` for "I am accessing thread X from thread Y, `BEFORETH` for "I am touching this threads data before any threads were spawned", &c). Oh, the definition of the `th` argument in functions was baked into a macro so it looked like `int foo(TH(main), int something, char else)`. `TH` also added some attributes - I know there was `unused`, there might have been others... Hopefully that gives some sense of it. Let me know if you have further questions! :)
- versteegen 11y agoThank you for that write-up! Sorry that my response is so slow. (Seems I can still reply several days later, wonder what the limit actually is) That's definitely both a nice solution to the problem, and I found it pretty enlightening (having sadly not used anything as strongly typed as Haskell). In the case of global variables, if performance didn't matter then it would be possible to just pass around a pointer to a struct of the thread-local/thread-locked data for use directly. I spent a while trying to come up with some alternatives or generalisation of your approach, and found it was really hard to match the same guarantees without a run-time cost. One idea I got from your description is putting access to certain data/function in scope in a type-checked way. So you could put multiple such contexts (pointers or other access methods like functions or even macros) in scope at the top of the function/block, rather than having to type-check every access throughout the function using an implicit control (e.g. the 'th' variable.) But really, that's just equivalent to using the L macro like so: void inc_foo(video_thread_t th) { video_thread_data_t *vid = &L(video_data); // checked global access vid->foo++; vid->bar++; } I think something like this looks more natural though.