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
$ node os-events.js
setup done.
Hello Node.js-World!
^Cexiting...
$ ./os-events
setup done.
Hello World!
^Cexiting...

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.

  1. Register a 11-day dummy keepAliveTimer to keep the program from exiting early.
  2. Define a worker function quitAfterGreetingPerson which prints a greeting to the console before killing the keepAliveTimer.
  3. 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.

  1. Register a timer to call it's handler one second after program start.
  2. The handler prints out the first part of the quote and then schedules another timer with one second delay
  3. 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.

Fork me on GitHub