RFC: A new kind of async
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 3.4k
- Forks
- 531
- Avg merge
- 21m
- Merged PRs (30d)
- 1
Description
I always felt that the AsyncWorkers are suffering from a design defect. Right at its very core is a very subtle anti-pattern. I call it "composition by inheritance". Does that sound oxymoronic? Good. It always starts like this:
class AbstractSomething {
virtual void doThing() = 0;
};
If you think about adding a feature by inheritance and about exposing both AbstractSomething and FeatureSomething to the user of your library, stop right there. You are almost certainly about to commit "composition by inheritance".
Another symptom of the anti-pattern is daisy-chaining. These class hierarchies never branch. Nobody wants to cut off a feature. Instead both sides, the library developers and the users, start accepting overhead. It becomes acceptable to tap into the class daisy chain at a point that gives you the features you need. If you inherit a couple of things you don't need, so be it.
It becomes acceptable because the alternative is a proliferation of Somethings covering more feature permutations. This is so undesirable that we accept the overhead. Note, how the combinatory nature of our features has "toolbox" written all over it. Instead, "composition by inheritance" always creates a monolith. While the problem at hand requires loose coupling, our design uses the tightest coupling possible: inheritance.
So, what now? A few weeks back i started hacking on a little sketch exploring new ways to write asynchronous node addons. Beside loose coupling and a more toolboxy approach i was looking for a design that is familiar to node developers.
In javascript it would seem natural to pass work functions and done handlers. So let's do this in C++. C++ already has a concept named callable. Relying on a language concept provides just the kind of loose coupling we are seeking. No inheritance, not even a real API. If it is callable, we're good.
Thinking about designs that take an arbitrary callable and execute it on the thread pool, another API comes to mind: Apple's grand central dispatch. Both environments have the notion of a main thread and they both have a thread pool. This thought gave the sketch its current direction and a name: ncd – not central dispatch. Please take a look and let me know what you think.
From the (current) README:
void
eventEmittingWorker(Nan::FunctionCallbackInfo<Value> const& args) {
using namespace std::string_literals;
unsigned delay = args[0]->Uint32Value();
unsigned iterations = args[1]->Uint32Value();
ncd::AsyncEventEmitter emitter(args[2].As<v8::Object>()); // ①
ncd::defaultWorkQueue().dispatch([=](){ // ②
for (unsigned i = 0; i < iterations; ++i) {
emitter.emit("progress"s, i); // ③
usleep(delay);
}
}, std::bind(emitter.emit, "done"s)); // ④
}
After grabbing some arguments the code creates an AsyncEventEmitter in (1). It wraps a javascript EventEmitter for use on a different thread. In (2) a lambda expression is dipatched to the thread pool. The expression captures copies of delay, iterations and the emitter. This is an ncd pattern: An AsyncSomething is first allocated on the main thread and then copied around to different threads. The call to dispatch(...) returns immediately and the lambda is launched on the thread pool. In (3) the async event emitter is invoked, sending progress events back to javascript. After the execution finishes the callback passed in (4) is invoked on the main thread. Here we just emit a done event.
The javascript code looks like this:
const workers = require('workers')
, EventEmitter = require('events')
, ee = new EventEmitter()
. on('progress', (p) => { console.log('progress', p) })
. on('done', () => { console.log('done') })
workers.eventEmittingWorker(10000, 10, ee)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing AsyncWorker implementation and the README sketch described in the issue, then compare its inheritance-based API with the proposed ncd callable-based design. The issue is an open-ended RFC pointing to an external ncd project, so it does not define a concrete entry point, tests, or acceptance criteria for completion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, javascript, node.js
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100