I would like to run tasks in parallel. At this time, I am using a very simple
worker pool using a single concurrent queue shared by all the threads.

Every task has a non unique "tag" (an integer in my case).
I would like tasks having the same tag not to run in parallel.

At this time, I have implemented it by having a `Runner` object for each
tag. New tasks are sent to the `Runner` associated with the task tag.

The `Runner` then enqueue the task into an internal queue and the checks
if it is already scheduled into the worker pool. If not, it schedules
itself into the pool.

When run inside the pool, it will pop a task from its internal queue,
run it and if the queue is not empty, reschedule itself.

Most operations (push, pop, already running check using a simple
boolean and queue is not empty check) are "protected" using a mutex
for each `Runner` instance. 

My solution seems to be working on a few simple cases, but I fear it
is somewhat fragile (for example, I'm almost certain that there is a
race condition in the "queue is not empty" check) and uses a lot of
locks (one for the main queue and for each tag).

Is there a cleaner solution, maybe less race condition-prone?

Here is a sample code illustrating my implementation (it is probably not working, as I'm replying from home, but I hope it is enough to get the idea):

```cpp
class Task {
public:
        Task(int tag) : tag_(tag) {};
        void run() {
                // do something
        }

        int getTag() { return tag_; }

private:
        int tag_;
};

class Runner;

class WorkerPool {
public:
        void scheduleRunner(Runner *runner);
};

class TaskQueue {
public:
        void push(std::shared_ptr<Task> task);
        std::shared_ptr<Task> pop();
        bool isEmpty();
};

class Runner {
public:
	Runner(WorkerPool& pool) : pool_(pool) {};

	// schedule execution of the task.
	// called by producers
	void scheduleTask(std::shared_ptr<Task> task) {
		std::unique_lock<std::mutex> lock(lock_);
		queue_.push(std::move(task));
		if (!running_) {
			pool_.scheduleRunner(this);
			running_ = true;
		}
	}

	// run the task from the pool
	void runTaskFromPool() {
		std::unique_lock<std::mutex> lock(lock_);
		std::shared_ptr<Task> task = queue_.pop();
		// we can't leave it locked because the task may need
		// to enqueue another element
		lock.unlock();
		task->run();
		lock.lock();
		if (queue_.isEmpty())
			running_ = false;
		else
			pool_.scheduleRunner(this);
	}

private:
	WorkerPool& pool_;
	TaskQueue queue_;
	std::mutex lock_;
	bool running_;
};

class Dispatcher {
public:
	Dispatcher(WorkerPool& pool) : pool_(pool) {};
	
	void scheduleTask(std::shared_ptr<Task> task) {
		std::unique_lock<std::mutex> lock(lock_);
		createRunnerIfNotExists_(task->getTag());
		runners_[task->getTag()]->scheduleTask(std::move(task));
	}

private:
	std::map<int, std::unique_ptr<Runner>> runners_;
        WorkerPool& pool_;
	std::mutex lock_;
};
```