Hacker News new | ask | show | jobs
by afc 2222 days ago
Interesting read.

I'm not sure I agree with the conclusion: I find using futures with very carefully controlled threading a preferable paradigm (for how I structure my programs in C++, at least for my text editor: https://github.com/alefore/edge).

In my experience, using sync code looks more readable on the surface but just kicks the can down the road: you'll still need to deal with the complexity of threading, and, in my experience, it's going to be waaay uglier. What you gain with your "superficial" simplicity, you pay a hundred times over with the algorithmic complexity of having to use threads with shared state. Every time you're troubleshooting some weird race condition that you can't easily reproduce you'll be wishing you had just used futures.

What I do is that the bulk of my processing runs in the main thread and I occasionally dispatch work to other threads, making sure that no mutable state is shared. When the "async" work is finished, I just have the background threads communicate the results to the main thread by setting a future (i.e., scheduling in the main thread the execution of the future's consumer on the results). Async IO operations are modeled just the same. In the beginning I had used callbacks spaghetti (mostly writing things in continuation passing style), but I started trying futures and found them much nicer.

I'll admit that on the surface this makes my code look slightly uglier than if it was directly sync code; however, it allows me to very safely use multiple threads. I think not having to make my classes thread safe (I typically stop at making them thread-compatible) and not having to troubleshoot difficult race conditions (and not having to block on IO or being able to easily run background operations) has been a huge win. If I had to rewrite this from scratch, I'd likely choose this model again. My editor only needs to use mutexes and such in very very few places; it suffices to make my classes thread-compatible and to ensure that all work in threads other than the main thread happens on const objects (and that such objects don't get deallocated before the background threads are done using them).

I rolled out my own implementation of futures here: https://github.com/alefore/edge/blob/master/src/futures/futu... (One notable characteristic is that it only allows a single listener, which I found worked well with "move" semantics for supporting non-copyable types that the listener gets ownership of.)

Here is one example of where it is used: https://github.com/alefore/edge/blob/5cb6f67e1e0726f8fbe12db...

In this example, it implements the operation of reloading a buffer, which is somewhat complex in that it requires several potentially long running or async operations (such as the evaluation of several "buffer-reload.cc" programs, opening the file, opening a log for the file, notifying listeners for reload operations...). It may seem uglier than if it was all in sync code, but then I would have to either block the main thread (unacceptable in this context) or make all my classes thread safe (which I think would be significantly more complexity).

I think this is cleaner than callbacks spaghetti because the code still reflects somewhat closely its logical structure. For loops I do have to use futures::ForEach functions, as the example shows, which is unfortunate but acceptable. In my experience, with callbacks spaghetti, it is very difficult to make code reflect its logical structure.