pioneerspacesim/pioneer — pull request #2758 by lwho (2014-02-14 19:11:44+00:00)
Title: Automatically manage the cancellation of jobs with RAII.

# Motivation

Not long ago I cleaned up ```Job``` cancellation a bit to avoid problems when jobs were still running when exiting a game. It turned out, that on one hand it was a bit fiddly and delicate to do correctly, and on the other hand it will be quite similar for every use. So, it should be done once and for all and then it should just be reused.

# How to use

This describes all you need to know to use the new API for managing ```Jobs``` in your class. If you are interested how it works internally, refer to the *Details* chapter below.

## Single job

If your class only needs a single job at a time, do the following (see ```GeoPatch``` for an example):

1. Store a job handle in your instance: ```JobHandle m_job;```
2. Queue a job: ```m_job = Pi::Jobs()->Queue(new MyJob(args));```. This is actually a move, so if you want to be explicit you can also say ```m_job = std::move(Pi::Jobs()->Queue(new MyJob(args)));```
3. If the job finishes normally, it will just clear the state of the ```JobHandle```.
4. If you assign (move) a new handle, the associated old job will be canceled.
5. If the ```JobHandle``` is destroyed (e.g. by the containing object being destroyed) the associated job will be canceled.

## Many jobs

If you may have multiple jobs at the same time, you can use the following scheme (like ```SectorCache``` does):

1. Store a job set in your instance: ```JobSet m_jobs;```
2. Tell the set in which queue it should put the jobs when constructing it: ```MyClass::MyClass() : m_jobs(Pi::Jobs()) { }```
3. Queue a job: ```m_jobs.Order(new MyJob(args));```
4. If the job finishes normally, its handle will be removed from the ```JobSet```.
5. If the ```JobSet``` is destroyed (e.g. by the containing object being destroyed) the contained ```JobHandles``` will be destroyed, which leads to their ```Jobs``` to be canceled.

# Details

Jobs running in sub-threads are just resources. And you should not have to manage resources manually in modern C++, that's what RAII (Resource Acquisition Is Initialization) is for. The resource is encapsulated in an object and the destructor of the object ensures that the resource is cleaned up properly.

## JobHandle

```JobHandle``` is such an RAII class encapsulating a running ```Job```. A ```JobHandle``` object will be returned from ```JobQueue::Queue(Job*)```. This handle remembers the job and the queue and when the ```JobHandle``` is destroyed, the job will be canceled. If the job finishes naturally, it will deregister from its handle (effectively emptying it). There can only ever be one ```JobHandle``` for a queued ```Job```, so they can not be copied, only moved.

## JobClient

When you only need one ```Job``` at a time, the ```JobHandle``` is all you need. Just put one in the instance of your class. (This is the ```GeoPatch``` case).

However, if you need multiple jobs (such as in ```SectorCache```) thinks will become more complicated. You could just use a container of ```JobHandles```. But when the ```Jobs``` finished naturally, this container would accumulate empty (orphaned) ```JobHandles```. So, it would be nice, if we were told when our ```Job``` is removed.

That's what ```JobClient``` is for. It is an abstract base class having to methods

1. ```void Order(Job*)``` to enqueue a passed ```Job``` and internally manage its ```JobHandle```
2. ```void RemoveJob(JobHandle*)``` is a callback called, when the ```Job``` finished and the client is supposed to remove the job.

```JobQueue::Queue(Job*, JobClient*)``` has actually a second (optional) argument and ```JobClient::Order``` is supposed to pass itself to be notified of removal.

### JobSet

You don't have to sub-class ```JobClient``` yourself. There already is a concrete sub-class ```JobSet``` which hopefully will be sufficient for most cases. It just acts as a container for ```JobHandles``` (internally using a ```map```, but that's just detail and can change).

- ```JobSet::Order(Job* job)``` will just call ```Queue(job, this)``` on the queue that it was passed on construction and will store the returned handle in its map.
- ```JobSet::RemoveJob(JobHandle* handle)``` will just erase the given handle from its map.