Execution threads

x::run() adapts std::thread and std::packaged_task to LibCXX's reference-counted object model. x::run() takes a reference to an object with a run() method, which gets invoked in a new thread.

#include <x/obj.H>
#include <x/ref.H>
#include <x/threads/run.H>

class buttonObj : virtual public x::obj {

public:

   bool run(int x, int y);

   buttonObj() noexcept;
   ~buttonObj();

   // ...
};

typedef x::ref<buttonObj> button;

// ...

button okButton=button::create();

// ...

x::runthread<bool> ret=x::run(okButton, 100, 100);

// ...

if (ret->terminated())
   // ...

ret->wait();

bool status=ret->get();

The first parameter to x::run() is a reference to an object that itself implements a run() method, possibly with parameters. x::run takes the reference to an object, and invokes the referenced object's run(), forwarding to it the remaining arguments to x::run().

The referenced object's run() gets invoked in a new execution thread, meanwhile x::run() returns a x::runthread<T>. The arguments to run() must be copy-constructible. x::run() copies the arguments, and the thread object's run() gets invoked with the copied arguments, which get destroyed after run() returns, and just before the new execution thread terminates.

x::run() takes care of instantiating a new std::thread, and joining the thread when it's done. With x::run(), there is no concept of joinable and detached threads. The thread started by x::run() holds its own reference on the object whose run() method got invoked, and releases the reference when run() returns. The calling thread may keep its reference to the object, for other purposes, or all of its references to the object can go out of scope, but the new thread's reference keeps the object from getting destroyed until run() returns.

When run() returns, an internal cleanup thread takes care of joining the terminated thread.

Checking the results of an execution thread

x::run() returns a x::runthread<T>, where T is the type of the return value from the thread object's run(). Its terminated() method returns a boolean that indicates whether run() in execution thread started by x::run() returned and the thread has terminated. get() waits for the run() method to return, and if T is not void, returns its return value (the return type of run() must be, naturally, copy-constructible.

If the execution thread terminated with an exception, rather than returning from its run(), get() rethrows that exception. wait(), wait_for(), and wait_until() wait until the execution thread terminates (either by returning from its run(), or by throwing an uncaught exception). wait() waits indefinitely, until run() returns. wait_for() and wait_until() take a std::chrono::duration or a std::chrono::time_point, respectively, setting the timeout for the wait.

Note

All execution threads are expected to terminate before the main application exits by returning from main() or by calling exit(). A warning message gets printed to standard error if not, and abort() follows if the remaining threads still do not terminate after thirty seconds.

x::runthread<T> refers to a template class that inherits from a non-template x::runthreadbaseObj class, whose reference type is x::runthreadbase. It implements just the terminated() and wait() methods. In contexts where the return value from an object's run() is not required, this allows implementation of non-templated code that waits for arbitrary execution threads to terminate.

Warning

pthread_cancel(3) cannot be used in code that uses LibCXX. pthread_cancel(3) terminates a thread without unwinding the stack. The stack may contain references to reference-counted objects, or other objects whose destructors contain critical functionality.

Using pthread_cancel(3) will result in memory leaks, deadlocks, and other unstable behavior. This is likely to be the case with any C++-based process of non-trivial complexity, not just LibCXX. The only safe way to forcibly terminate the thread is by throwing an exception that unwinds the entire stack frame. x::msgdispatcherObj provides a convenient message-based thread design pattern which supplies a stop() method that sends a message to the running thread that causes it to throw an exception and terminate, in an orderly manner.

Arguments to run() methods must be copy-constructible

It's already been mentioned that arguments to execution threads' run() methods must have a copy constructor.

#include <x/obj.H>
#include <x/ref.H>
#include <x/threads/run.H>

class widget;

class rendererObj : virtual public x::obj {

public:

    void run(widget &w);

// ...
};

void runawidget(const x::ref<renderObj> &r,
                const widget &w)
{
    x::run(r, w);
}

In this example, the thread object's run() method takes a reference to a non-constant widget object. However, the argument to x::run() is a reference to a constant widget object.

This is because the arguments get copied for the new execution thread. The thread object's run() method get invoked with the thread's copy of each argument passed to x::run(). The run() method's prototype can specify either a constant reference, or a mutable reference to a type, in either case it'll get resolved.

This means that although an object may have overloaded run() methods, a run() that takes a reference to a constant object will never be used if there's an overloaded run() method that takes a reference to a mutable object (as long as all other parameters are the same).

template<typename objClass, typename baseClass, typename ...Args>
auto start(const x::ref<objClass, baseClass> &thread,
           Args && ...args)
-> typename x::runthread<decltype(thread->run(x::invoke_run_param_helper
	<Args>()...))>

// ...

As described previously, x::run() returns a x::runthread<T>, where T gives the return type of the object's run(). Sometimes its useful to define a template that uses a related type, such as a corresponding x::ptr, instead. x::invoke_param_helper() helps in construing the parameters to a run as if they were invoked in the executed thread, given the original parameters to x::run(). Its definition is very short:

template<typename param_type> class run_param {

public:
    typedef typename std::decay<param_type>::type &type;
};

template<typename param_type>
typename run_param<param_type>::type &invoke_run_param_helper();

Each parameter to x::run() is decay-copied into the new execution thread's context, then passed by reference to a run() method.

Optional superclasses of thread-executing objects

To supply a default thread name for logging purposes, subclass from x::runthreadname and implement getName(), as follows:

#include <x/obj.H>
#include <x/runthreadname.H>

class myThreadObj : virtual public x::obj, public x::runthreadname {

public:

// ...

    void run( ... );

    std::string getName() const { return "mythread"; }
};

x::run() invokes getName() prior to invoking the thread object's run(), to set the thread's name, for logging purposes.

By default, x::run() has no issues with starting multiple concurrent threads, at the same time, using the same class instance. To prevent that, subclass x::runthreadsingleton:

#include <x/obj.H>
#include <x/runthreadsingleton.H>

class myThreadObj : virtual public x::obj, virtual public x::runthreadsingleton {

// ...

};

x::run() now throws an exception if an earlier x::run() spawned a thread on the same object, and the earlier thread is still running. Use virtual public inheritance to subclass x::runthreadsingleton, so that a subclass that inherits from multiple thread singletons gets a single instance of x::runthreadsingleton.

Running a lambda inside a separate thread

auto ret=x::run_lambda([]
    (const x::netaddr &addr)
    {
        x::fd sock=addr->connect();

        return sock;
    }, x::netaddr::create("localhost", "http"));

ret->wait();

x::fd val=ret->get();

x::run_lambda() starts a new thread that starts running the lambda given as its first parameter, then returns a x::runthread<T>, that can be used to wait for the lambda to finish, then retrieve the return value from the lambda.

Cleanup thread

x::run() takes care of the hard work of using std::thread directly, notably the requirement to join the started thread before std::thread's destructor gets invoked. std::thread is privately owned by a x::runthread<T> that gets returned by x::run(). Additionally, the started thread maintains its own reference on the thread object, and the x::runthread instance.

If all other references to the thread object go out of scope, the thread object does not get destroyed before its run() returns, since it holds the last remaining reference to the thread object. Furthermore, there's also an indirect reference on the x::runthread, with its private std::thread instance, that does not get released until after run(), at which point its turned over to a cleanup thread.

The first call to x::run() starts a separate thread that joins all threads that return from thread objects' run()s. A single cleanup thread gets used for all x::run()s-started execution threads.

A terminated thread's x::runthread, containing its std::thread, gets turned over to the cleanup thread, which joins it, sets its x::runthread's terminated() to true, and makes the return value from the thread object's run() get()able from the x::runthread.

Even if the x::runthread that's returned by x::run() gets ignored, an internal reference to it gets held by the executing thread. This keeps it from getting destroyed until run() returns and the thread terminates. When that happens, the x::runthread gets turned over to the cleanup thread, which joins the terminated thread, and then releases the last reference on the x::runthread, finally destroying it.

For this reason, the x::runthread could, but should not be used as a mcguffin. The cleanup thread is unable to continue joining other threads until any destructor callbacks return.

Stick a fork() in it: you're done

fork() invalidates all threads and all thread support library classes. They are used extensively by LibCXX, so after a fork(), the child processes cannot use any LibCXX classes or methods.

fork() has no effect in the parent process. All threads remain valid. All thread objects remain valid.

Use x::forkexec to safely fork() and exec() another process.

See also: Chapter 15, Forking other processes.

Thread logging

The full log message format generated by the application logging subsystem includes the name of the running thread. This is set by getName() method of the thread object passed to x::run(), if the thread class inherits from x::runthreadname, or a default name.