Operating System Event Loop
Cpp
#include <boost/asio.hpp>
#include <csignal>
#include <iostream>
#define BOOST_THREAD_VERSION 4
#include <boost/thread/future.hpp>
using namespace std;
namespace asio = ::boost::asio;
int main() {
asio::io_context ioc;
asio::signal_set terminationSignals(ioc, SIGINT, SIGTERM);
auto shutdown = [&](const boost::system::error_code& ec, int ) {
if (!ec) {
terminationSignals.cancel();
}
};
terminationSignals.async_wait(shutdown);
asio::post(ioc, [](){
cout << "Hello World!" << endl;
});
cout << "setup done." << endl;
// This method blocks the main thread.
ioc.run();
cout << "exiting..." << endl;
return 0;
}
JavaScript
/****** Browser ******/
// Nothing to do here: The process runs as long as the
// browser tab is not closed.
setTimeout(function(){
console.log("Hello Browser-World!");
}, 0);
/******Node.js *******/
// run for 11 days
const runTimer = setTimeout(function keepAlive(){}, 1.0e+9);
function shutdown() {
console.log("exiting...");
clearTimeout(runTimer);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.nextTick(function(){
console.log("Hello Node.js-World!");
});
console.log("setup done.");
What This Code Does
Demonstrate the use of lambda functions as callbacks for basic asynchronous tasks.
- Scheduling a Hello-World-lambda for immediate deferred execution
- Quitting the program on an external
KILL
signal
It is common for a program to instruct the operating system to perform long-running computations or to respond to events from external sources. A long running computation could be copying a large file or scheduling a timer. External events can be user inputs in a GUI program, or for a server program the activity in the network for example. To avoid getting unresponsive an application typically delegates large work loads into parallel running threads or the operating system. To consume the outcome of such an operation the program must run a loop which listens to events coming from the operating system or own threads.
To reduce the complexity of a program and to make it less error-prone, it is advisable to leave the management of threads and event loops to well established third-party libraries.
What's the Same
Both examples set up an environment to support asynchronous execution. Both programs run non-blocking down to the bottom of the program without any output. We keep the program running unless the program is sent a termination signal.
JS output | C++ output |
|
|
What's Different
Node.js
Being essentially single-threaded JavaScript can rely on it's engine for all parallel executing multi-threading stuff. Today lambda callbacks, promises, async/await etc. are baked into the language to help with asynchronous operations. The Node.js event loop starts as soon as the process starts, and ends when no further callbacks remain to be performed.
The program is kept running using a practically never-ending timer. This extra step is needed in this example because
when process.on
adds it's callback to the JS event-loop the program has already exited.
C++ with Boost.Asio
Boost.Asio
provides a platform-independent asynchronous api for common long-running tasks in networking, timers, serial ports and signals.
The outcome events are typically processed by continuations (callback functions) or can be forwarded to std::future
.
Boost.Thread v.4 provides a implementation for
future continuations as proposed in C++14 TS Extensions for Concurrency V1. C++ Futures are a general high-level abstraction for background threads.
With the .then
continuations they have much in common with the promises of JS.
The program is kept running as long as there are pending operations (i.e.)terminationSignals
scheduled to asio::io_context
.
Delay or Schedule Computations
Cpp
#include <iostream>
#include <chrono>
#include <memory>
#include <boost/asio.hpp>
using namespace std;
namespace asio = ::boost::asio;
int main() {
asio::io_context ioc;
auto keepAliveTimer = make_shared<asio::steady_timer>(
ioc, chrono::hours(11 * 24));
auto quitAfterGreetingPersonToo =
[keepAliveTimer](const boost::system::error_code& ec,
const string& name) {
if (!ec) {
cout << "Hello " << name << "!" << endl;
keepAliveTimer->cancel();
}
};
// timer begins to run now
auto greetTimer = asio::steady_timer(ioc, chrono::seconds(1));
// later we can attach the completion handler
greetTimer.async_wait(bind(
quitAfterGreetingPersonToo,
std::placeholders::_1,
"World"));
ioc.run();
return 0;
}
JavaScript
// run for 11 days
const keepAliveTimer = setTimeout(function keepAlive(){}, 1.0e+9);
function quitAfterGreetingPerson(name) {
console.log("Hello " + name + "!");
clearTimeout(keepAliveTimer);
}
const greetTimer = setTimeout(quitAfterGreetingPerson, 1000, "World");
What This Code Does
Delaying or scheduling the execution of functions, lambdas or methods on the same thread without blocking.
- Register a 11-day dummy
keepAliveTimer
to keep the program from exiting early. - Define a worker function
quitAfterGreetingPerson
which prints a greeting to the console before killing thekeepAliveTimer
. - Schedule the execution of the worker providing a parameter after one second.
What's the Same
The building blocks when registering function for execution through timers is the same. One has to specify a
- delay,
- a lambda, free function,
std::function
or bound member function and - additional parameters
What's Different
In C++ the parameters need to be provided by std::bind
while in JavaScript they are appended as
the setTimeout
arguments. (Though one could use setTimeout(quitAfterGreetingPerson.bind(undefined, "World"), 1000)
to make it look more like in C++).
In C++ the definition of the timer and the registering of the worker are separate. Thus under circumstances a timer may already be expired before a handler is registered. This is not possible in JavaScript.
C++ is very different from JavaScript in that not only the expiring, but also the canceling of a timer invokes
the same handler.
Cancellation is reported by the error code parameter e.g. boost::asio::error::operation_aborted
.
Therefore the error code needs to be checked before performing the actual scheduled computations
Destroying a Boost.Asio timer object cancels any outstanding asynchronous wait operations associated with the timer
as if by calling timer.cancel()
.
Thus to achieve similarity with JavaScript
when creating timers in C++ block scopes, e.g. lambdas, one should rather create timers on the heap with new
and share them via strong reference std::shared_ptr
with their handler. This way the timer will not
be destructed until the handler was called (i.e by timer expiry or cancellation).
Nesting Scheduling of Asynchronous Computations
Cpp
#include <iostream>
#include <chrono>
#include <memory>
#include <boost/asio.hpp>
using namespace std;
namespace asio = ::boost::asio;
int main() {
asio::io_context ioc;
auto stepTimer = asio::steady_timer(ioc, chrono::seconds(1));
stepTimer.async_wait([&ioc](const boost::system::error_code& ec) {
if (!ec) {
cout << "That's one small step for a man," << flush;
auto leapTimer = make_shared<asio::steady_timer>(
ioc, chrono::seconds(1));
// WITHOUT CAPTURING leapTimerPtr the handler would be
// invoked IMMEDIATELY with non-zero error code
leapTimer->async_wait(
[leapTimer](const boost::system::error_code& ec) {
if (!ec) {
cout << " one giant leap for mankind." << endl;
}
});
}
});
ioc.run();
return 0;
}
JavaScript
setTimeout(function(){
process.stdout.write("That's one small step for a man,");
setTimeout(function() {
console.log(" one giant leap for mankind.");
}, 1000);
}, 1000);
What This Code Does
Nesting the execution of scheduled lambdas on the same thread without blocking is demonstrated by writing the two sub sentences of Neil Armstrong's Moon-Landing quote delayed in time.
- Register a timer to call it's handler one second after program start.
- The handler prints out the first part of the quote and then schedules another timer with one second delay
- to print out the rest of the quote.
What's the Same
The nesting of asynchronously executed lambdas is demonstrated in both languages.
What's Different
In C++ the destruction of a Boost.Asio timer cancels any pending asynchronous wait operations. Since exiting a C++ block scope calls the destructor of all stack objects, we avoid this for the inner timer by creating it on the heap and manage access and lifetime by a C+ shared pointer created on the stack. In order to prevent it's reference count drop to zero and get the timer object destroyed nevertheless when the block scope exits, we have to pass a copy of it into the scheduled timeout handler.
This looks awkward because the timer object is not really used in it's completion handler. While JavaScript itself manages the lifetime of it's reference objects - like timers -; in C++ the reference counting is left to the application.
Also C++ requires checking the error code parameter of the handler to distinguish timer expiry from timer cancellation.