A platform-independent promise library for C++, implementing asynchronous continuations.
Tested on Linux x86-64 (GCC, LLVM), macOS x86-64 (XCode), Windows x86 and x86-64 (Visual Studio 2015) and on Android ARM (Android Studio). Supposed to work on FreeBSD (and potentially other BSD's too).
Still unversioned until closing in on 1.0. The API is likely not going to change until 1.0.
Apache License 2.0
The library q (or libq) is following the core ideas and naming in the Promises/A+ specification, which provides a simplistic and straightforward API for deferring functions, and invoke completions asynchronously. The name is borrowed from the well-known JavaScript implementation with the same name, but is influenced more by Bluebird by providing a lot of functional tools such as filter
, map
and reduce
(yet to be implemented / decided to be implemented) as well as finally
, reflect
and tap
. Although q provides a large amount of features, they all exist to make the promise implementation as solid and easy-to-use as possible. A generic and useful event based routine delay
is implemented in core q, but more high-resolution timer events as well as other event based asynchronous I/O (e.g. file or network), is not built-in. Instead, this is provided by support libraries (like q-io). This is to keep q
free from any dependencies, it only depends on C++11. The built-in delay
is supported by the blocking_dispatcher
and threadpool
, but are of low-resolution system timers.
q provides, a part from the pure asynchronous IoC methods, a wide range of tools and helpers to make the use as simple and obvious as possible, while still being perfectly type safe. One example if this, is the automatic std::tuple
expansion of promise chains.
The concept of q is that tasks are dispatched on queues, and a queue is attached to an event_dispatcher
. q comes with its own blocking_dispatcher
, which is like an event loop, to dispatch asynchronous tasks in order, but it is also easily bridged with existing event loops, such as native Win32, GTK, QT or Node.js. However, q also comes with a thread pool, which also is an event_dispatcher
.
One of the most important reasons to use q is that one can run tasks not only asynchronous, but also on different threads, without the need to use mutexes and other locks to isolate data. Instead, one will perform tasks on certain queues which dispatches the tasks only on certain threads. This separation of logic and execution provides a means to decide on which thread a certain function should be called, allowing shared state to always be accessed from the same thread. It also provides fantastic means to, at any time, change what logic is supposed to run on what threads to optimize the program for performance.
q primarily features a promise implementation that is fast, thread safe and type safe. The interface exposes a very simple syntax which makes the code flow natural and easy to understand and follow, something traditional callback style programming soldom does.
However, q comes with a wide set of optional features surrounding this:
async_termination
channel< T >
event_dispatcher
exception
std::iostream
(even raw std::exception_ptr
's).q::all( )
.expect< T >
T
), or an exception.functional
call_with_args
, call_with_args_by_tuple
, etc
std::tuple
's.memory
make_shared( )
feature similar to std::make_shared
but allows one to have protected constructors to enforce the use of make_shared
, without having to be friend with std::make_shared
. Also allows one to override the constructor with a custom construct( )
function which will be called instead, when doing q::make_shared
.mutex
std::mutex
classes, but enforcing names and providing dead-lock detection (not implemented yet).promise
queue
scheduler
s which runs on top of event_dispatcher
s, such as the simple blocking_dispatcher
or threadpool
s.scope
stacktrace
temporarily_copyable
thread
mutex
, this is similar to the standard's std::thread
, but providing very useful extra features:
async_termination
, allowing the owner of the thread to wait for its completion and retrieving the result asychronously, scheduled with a promise
.run( name, fn, args... )
helper function, which just runs a function with arbitrary arguments and returns a promise
with the returned value, which one can .then( )
schedule completions for.threadpool
type_traits
bool_type< bool >
which is std::true_type
or std::false_type
.negate< Fn >::of< ... >
which negates meta functions which results in std::true_type
or std::false_type
.isnt< >
, is_tuple< >
, is_copyable< >
, is_movable< >
, is_pointer_like< >
, tuple_arguments< >
, tuple_unpackable_to< >
, variadic_index_sequence
...arguments< ... >
fold
and two_fold
logic_and
, logic_or
, logic_eq
, generic_operator
...hierarchical_condition
fold
to fold hierarchically over sets of types, and where std::tuple
packed types are unpacked hierarchically. This is useful when checking if all types in a tuple, and its sub-tuples satisifies arbitrary conditions, and is a necessary core feature on which q heavily depends.hierarcichally_satisifies_condition
, hierarchically_satisfies_all_conditions
, hierarchically_satisfies_any_condition
is_copy_constructible
, is_move_constructible
, ...For a C++ programmer, it is likely easiest to learn and to understand the library by some examples.
The following example shows how q can be used for networking.
q::promise< std::string, std::string > read_message_from_someone()
{
return connect_to_server( )
.then( [ ]( connection& c )
{
return c.get_next_message( );
} );
}
...
read_message_from_someone( )
.then( [ ]( std::string&& username, std::string&& msg )
{
std::cout << "User " << username << " says: " << msg << std::endl;
} )
.fail( [ ]( const ConnectionException& e )
{
std::cerr << "Connection problem: " << e << std::endl;
} )
.fail( [ ]( std::exception_ptr e )
{
std::cerr << "Unknown error" << std::endl;
} );
Terminating an object means to put it in a state where it is not performing anything anymore, and can be freed. However, sometimes there are references to the object, or it is internally not done with its inner tasks. It could e.g. have a thread that is currently working. q comes with a class called async_termination
which can be subclassed, to provide a smooth way of terminating objects and waiting for them to complete.
auto object = q::make_shared< my_class >( );
object->perform_background_task( );
object->terminate( )
.then( [ ]( )
{
std::cout << "object has now completed and can safely be freed" << std::endl;
} );
Running a function on a newly created thread, "waiting" (asynchronously) for the function to complete, and then using the result in a function scheduled on another thread (or the main thread). Note that we don't need mutexes or semaphores.
q::run( "thread name", [ ]( )
{
// Thread function which can perform heavy tasks
return sort_strings( ); // Returns a vector of strings
} )
->terminate( ) // Will not really terminate, but rather await completion
.then( [ ]( std::vector< std::string >&& strings )
{
// The result from the thread function is *moved* to this function
std::cout << strings.size( ) << " strings are now sorted" << std::endl;
} );
Lets say you have multiple (two or more) tasks which will complete promises and you want to await the result for all of them. This is done with q::all( )
which combines the return values of the different promises and unpacks them as function arguments in the following then( )
task.
q::promise< double > a( );
q::promise< std::string, int > b( );
q::promise< > c( );
q::promise< std::vector< char > > d( );
q::all( a( ), b( ), c( ), d( ) )
.then( [ ]( double d, std::string&& s, int i, std::vector< char >&& v )
{
// All 4 functions succeeded, as all promises completed successfully.
} )
.fail( [ ]( std::exception_ptr e )
{
// At least one of the functions failed, or returned a promise that failed.
std::cerr << q::stream_exception( e ) << std::endl;
} );
A similar problem is having a variably sized set of same-type promises. Such can also be completed with q::all( )
, although the signature for the following then( )
task will be slightly different.
std::vector< q::promise< std::string, int > > promises;
q::all( promises )
.then( [ ]( std::vector< std::string, int >&& values )
{
// Data from all promises is collected in one std::vector and *moved* to this function.
// This means we can move it forward, and the data had never been copied.
do_stuff( std::move( values ) );
} )
.fail( [ ]( q::combined_promise_exception< std::string >&& e )
{
// At least one promise failed.
// The exception will contain information about which promises failed and which didn't.
} );
Unit testing an application (or library) with asynchronous logic is often cumbersome. The test must block until all asynchronous routines have completed, whether they are single-threaded or not.
q-test
is a library which helps with this, and provides simple helper macros, such as EVENTUALLY_EXPECT_EQ( a, b )
. q-test
has a peer dependency on the underlying unit test framework, meaning that you (potentially) need to link to it.
The unit test frameworks supported as backends to q-test
are Boost.Test, Catch, CppUnit, doctest and Google Test.
In your unit tests, you (likely) won't need to include the unit test headers of your chosen unit test framework, only <q-test/q-test.hpp>
(or preferably <q-test/expect.hpp>
). You need to configure it to use your unit test framework, by defining the backend before you include the <q-test/>
headers. The backends you can define are QTEST_ON_BOOST
(or QTEST_ON_BOOST_SH
for the single-header version), QTEST_ON_CATCH
, QTEST_ON_CPPUNIT
, QTEST_ON_DOCTEST
or QTEST_ON_GTEST
. It's a good idea to create a common unit test header which includes q-test
, containing e.g.
// "common.hpp"
#define QTEST_ON_CATCH // If you're using Catch
#include <q-test/q-test.hpp>
#include <q-test/expect.hpp>
If you want q-test
to create the main()
function, include q-test/main.hpp
in one of your translation units (.cpp
files). Note that you also need to prepend the chosen unit test backend.
#define QTEST_ON_CATCH
#include <q-test/main.hpp>
q-test
uses fixtures to store the necessary things to allow promises to function (a blocking dispatcher, threadpool, some queues, etc). This can be subclassed and is called q::test::fixture
, but there is also a macro to quickly create a fixture, Q_TEST_MAKE_SCOPE( fixture_name )
. What you need to know about this fixture is that you have two queues available, one single-threaded "main queue" called queue
, and a queue bound to a threadpool called tp_queue
. By using fixtures, your test will have these variables accessible on this
.
An example of a unit test file using google test, using the common header file described above, would be:
#include "common.hpp"
Q_TEST_MAKE_SCOPE( mytest );
TEST_F( mytest, some_promise_test )
{
auto should_be_10_str =
q::with( queue, 5 )
// The multiplication with 2 will be done on a threadpool
.then( [ ]( int i ) { return i * 2; }, tp_queue )
.then( [ ]( int i ) { return std::to_string( i ); } )
.share( );
EVENTUALLY_EXPECT_EQ( should_be_10_str, "10" );
}
The q-test
suite creates the macros EVENTUALLY_EXPECT_EQ
, EXPECT_CALL
etc. These macros may conflict with your existing macros, or a unit test framework's macros. You can use Q_
-prefixed versions of the macros instead (e.g. Q_EXPECT_CALL
), and have q-test
not generate the un-prefixed macros by defining QTEST_NO_PRETTY_MACROS
.
q uses CMake to generate build scripts.
The examples below build to a directory build
, to leave the root repository clean.
git clone https://github.com/grantila/q.git
cd q
cmake -G "Unix Makefiles" -Bbuild -H.
cd build
make
git clone https://github.com/grantila/q.git
cd q
cmake -G "Xcode" -Bbuild -H.
open build/q.xcodeproj
Visual Studio 2015 32-bit (using Git Bash shell):
git clone https://github.com/grantila/q.git
cd q
cmake -G "Visual Studio 14 2015" -Bbuild -H.
start build/q.sln
For other versions of Visual Studio, apply the appropriate CMake generator. For 64-bit, add Win64
to the generator name, such as cmake -G "Visual Studio 14 2015 Win64"
, and for ARM add ARM
: cmake -G "Visual Studio 14 2015 ARM"
.
When compiling with windows.h
included before q
, ensure to have NOMINMAX
defined:
#define NOMINMAX
#include <windows.h>