Home

Awesome

Lucy Job System

This is outdated compared to Lumix Engine. Use that instead.

Fiber-based job system with extremely simple API.

It's a standalone version of job system used in Lumix Engine. Only Windows is supported now, although the Lumix Engine version has Linux support, I have to copy it here yet.

Demo

Create a solution with create_solution_vs2017.bat. The solution is in build/tmp/. Open the solution, compile and run.

#include "lucy.h"
#include <condition_variable>
#include <cstdio>
#include <mutex>


std::mutex g_mutex;
std::condition_variable g_finished;
std::mutex g_finished_mutex;


void print(const char* msg)
{
	std::lock_guard lock(g_mutex);
	printf(msg);
	printf("\n");
}


void jobB(void*)
{
	print("B begin");
	print("B end");
}


void jobA(void*)
{
	print("A begin");
	lucy::SignalHandle finished = lucy::INVALID_HANDLE;
	for(int i = 0; i < 10; ++i) {
		lucy::run(nullptr, jobB, &finished);
	}
	lucy::wait(finished);
	print("A end");
	g_finished.notify_all();
}


int main(int argc, char* argv[])
{
	lucy::init();
	lucy::run(nullptr, jobA, nullptr);
	std::unique_lock<std::mutex> lck(g_finished_mutex);
	g_finished.wait(lck);
	lucy::shutdown();
}

Integration

Just put lucy.h and lucy.cpp in your project and you are good to go.

Docs

See src/main.cpp for an example.

Initialization / shutdown

lucy::init() must be called before any other function from lucy namespace lucy::shutdown() call this when you don't want to run any more jobs. Make sure all jobs are finished before calling this, since it does not wait for jobs to finish.

Jobs

Job is a function pointer with associated void data pointer. When job is executed, the function is called and the data pointer is passed as a parameter to this function.

lucy::run push a job to global queue

int value = 42;
lucy::run(&value, [](void* data){
	printf("%d", *(int*)data);
}, nullptr);

This prints 42. Eventually, after the job is finished, a signal can be triggered, see signals for more information.

Signals

lucy::SignalHandle signal = lucy::INVALID_HANDLE;
lucy::wait(signal); // does not wait, since signal is invalid
lucy::incSignal(&signal);
lucy::wait(signal); // wait until someone calls lucy::decSignal, execute other jobs in the meantime
lucy::SignalHandle signal = lucy::INVALID_HANDLE;
for (int i = 0; i < N; ++i) {
	lucy::incSignal(&signal);
}
lucy::wait(signal); // wait until lucy::decSignal is called N-times

If a signal is passed to lucy::run (3rd parameter), then the signal is automatically incremented. It is decremented once all the job is finished.

lucy::SignalHandle finished = lucy::INVALID_HANDLE;
for(int i = 0; i < 10; ++i) {
	lucy::run(nullptr, job_fn, &finished);
}
lucy::wait(finished); // wait for all 10 jobs to finish

There's no need to destroy signals, it's "garbage collected".

Signals can be used as preconditions to run a job. It means a job starts only after the signal is signalled:

lucy::SignalHandle precondition = getPrecondition();
lucy::runEx(nullptr, job_fn, nullptr, precondition, lucy::ANY_WORKER);

is functionally equivalent to:

void job_fn(void*) { 
	lucy::wait(precondition);
	dowork();
}
lucy::run(nullptr, job_fn, nullptr);

However, the lucy::runEx version has better performance.

Finally, a job can be pinned to specific worker thread. This is useful for calling APIs which must be called from the same thread, e.g. OpenGL functions or WinAPI UI functions.

lucy::runEx(nullptr, job_fn, nullptr, lucy::INVALID_HANDLE, 3); // run on worker thread 3