NoesisGUI

Multithreading primitives

Noesis Engine implements different multithreading primitives that permit implementing scalable algorithm in a portable way.

Threads

Threads are managed by the class Thread. Each instance of this component creates an operating system thread within the virtual address of the calling process. To create a thread, a Thread instance is constructed passing a delegate that represent the entry point of the thread. For example,

void Thread1Func(NsSize value)
{
    NS_INFO(NST("Thread %d created"), value);
}

void main()
{
    Ptr<Thread> thread1 = NsCreateComponent<Thread>("Thread1", MakeDelegate(&Thread1Func), 500);
    thread1->Join();
}

The Join() method is used to wait for the end of execution of the thread. If the thread instance is destroyed without Joining(), the thread is not stopped, it can continue execution.

The thread class offers other interesting functionality like Sleeping() the current thread, Yield() the current thread, getting the current thread id, etc. Read the associated documentation to get more information.

Data Locks

Locks can be used to protect shared data between threads. The mutex object encapsulate a lightweight synchronization that can only be owned by a thread. Mutex are supposed to hold a region for a very small period of time.

The Mutex implementation provides a helper class named Mutex::ScopedLock that automatically acquire/release the mutex in a context.

Example of usage:

{
    Mutex::ScopedLock lock;
    NS_INFO(NST("This region is protected by a mutex"));
}

Atomic Operations

Basic operations on primitive types do not need a mutex because they can be executed atomically using the Atomic<T> template. Atomic operations are thread-safe. For example,

void Add(Atomic<NsSize>& val)
{
    // This region is thread safe
    val++;
    val++;
    val++;
    val++;
    val++;
}

Tasks

The TaskSystem is a kernel system available for executing tasks. Tasks are one-shot objects deriving from the base class BaseTask. TaskSystem supports two kind of tasks: Asynchronous and Synchronous.

Asynchronous tasks

Asynchronous tasks are tasks whose execution is done in parallel with the main thread. An Asynchronous tasks is fired and you forget about it. The task is inserted into a queue and asynchronously executed (in a different thread than the main thread). This kind of tasks are ideal from low priority work than takes more than one frame in execute and whose result is not needed inmediately.

Each task can be send to a channel and tasks belonging to the same channel are executed in FIFO order. The default channel does not have this property and all the tasks located in that channel may be executed in parallel (in depends on the number of threads created by the Task system for this kind of tasks).

Whenever the execution of this kind of tasks is finished, the Retire() method is invoke from the main thread.

class TestTask: public AsyncTaskTest
{
public:
    TestTask(Atomic<NsInt32>& execCount, Atomic<NsInt32>& retireCount, NsInt id):
        mExecCount(execCount),
        mRetireCount(retireCount), mId(id)
    {
    }

    void Exec()
    {
        NS_INFO(NST("Test%d"), mId);

        for (NsInt32 i = 0; i <= mId; i++)
        {
            mExecCount++;
        }
    }

    void Retire()
    {
        for (NsInt32 i = 0; i <= mId; i++)
        {
            mRetireCount++;
            mRetireCount++;
        }
    }

private:
    Atomic<NsInt32>& mExecCount;
    Atomic<NsInt32>& mRetireCount;
    NsInt mId;

    /// Reflection
    //@{
    NS_BEGIN_REFLECTION(NSS(TestTask), TestTask, AsyncTaskTest)
    NS_END_REFLECTION

    NS_IMPLEMENT_INLINE_COMPONENT_REFLECTION
    //@}
};

void ExecTask()
{
    Ptr<TestTask> task0 = NsCreateComponent<TestTask>(execCount, retireCount, 0);
    Ptr<TestTask> task1 = NsCreateComponent<TestTask>(execCount, retireCount, 1);
    Ptr<TestTask> task2 = NsCreateComponent<TestTask>(execCount, retireCount, 2);
    Ptr<TestTask> task3 = NsCreateComponent<TestTask>(execCount, retireCount, 3);
    Ptr<TestTask> task4 = NsCreateComponent<TestTask>(execCount, retireCount, 4);

    taskSystem->ExecAsync(task0);
    taskSystem->ExecAsync(task1);
    taskSystem->ExecAsync(task2);
    taskSystem->ExecAsync(task3);
    taskSystem->ExecAsync(task4);
}

Synchronous tasks

Synchronous tasks have to be waited until the execution is finished. Typically a root task is added to the TaskSystem and that task is subdivided into smaller tasks that can be executed concurrently by several threads. In this kind of model the main thread waiting for the end of the execution may collaborate executing pending tasks. Synchronous tasks are not guaranteed to execute concurrently. All the tasks may be executed by the main thread. These tasks are executed using a work-stealing task scheduler as described in Cilk: An Efficient Multithreaded Runtime System. The order followed is LIFO as described in that paper to improve local thread cache coherency.

Inside the execution of a synchronous tasks you have three basic functions: CreateTaskChild(), to create a child of the current task, Spawn() to start executing a child task and Sync() that waits for the execution of all the spawned children.

class FibTask: public SyncTaskTest
{
public:
    FibTask(NsSize number, NsSize& res): mNumber(number), mRes(res) {}

    void Exec()
    {
        if (mNumber < 2)
        {
            mRes = mNumber;
        }
        else
        {
            NsSize x, y;

            Spawn(CreateChildTask<FibTask>(mNumber - 1, x));
            Spawn(CreateChildTask<FibTask>(mNumber - 2, y));

            Sync();

            mRes = x + y;
        }
    }

private:
    NsSize mNumber;
    NsSize& mRes;

    /// Reflection
    //@{
    NS_BEGIN_REFLECTION(NSS(FibTask), FibTask, SyncTaskTest)
    NS_END_REFLECTION

    NS_IMPLEMENT_INLINE_COMPONENT_REFLECTION
    //@}
};

void ExecTask()
{
    NsSize res;
    Ptr<FibTask> task = NsCreateComponent<FibTask>(25, res);
    taskSystem->ExecSync(task);
    NS_INFO(NST("Fib %d = %d"), 25, res);
}
© 2017 Noesis Technologies