Task Manager System

Started by
3 comments, last by jwein 15 years, 8 months ago
Hello, I am going to develop a task manager for my 3d engine and I have studied the way the task manager proposed in the enginuity series of articles works and I think I am looking for something different for different reasons... 1) I would like to avoid singletons, but the tasks classes have to use the task manager class ( kernel ) to stop themselves. Ok, this could be solved passing a pointer to the task manager to the different tasks when they are created, but I don't know if it's a good solution. 2) I would like to integrate the task manager in the engine itself. Enginuity's manager is good for a game-level architecture, but in its structure tasks have to use the engine subsystems to do the different operations they are supposed to do. I would like to develop an engine in which subsystems are connected to the engine whith some functions ( the engine shouldn't know anything about the different subsystems ) and in which the subsystems are the tasks themselves. My position could seem very confused and ... So it is :) I would like to ask you 1) what are the objectives of a task manager 2) if the direction i am going to is ok or if i am doing it wrong 3) if it's possible and convenient to abstract the engine from the subsystems ( so the core class could have a list of subsystems, which could derive from a base class, which could be useful to pass the pointer to a subsystem to another one and how could i do that. I would appreciate examples too :) Thank you!!
Advertisement
The basic point of a task system is to allow time-sharing between processes without incurring the overhead of normal multi-threading and without having to solve the same synchronization and atomicity issues. At its most elementary level, a task is a sequence of functions to be called one after another to perform a process, the advantage (when compared to performing the process with a single function call) being that you can 'pause' the execution of the task in-between function calls in order to do other things, and the disadvantage being that cutting up a process into independent functions is hard. As for a kernel, it is nothing more than a set of tasks with a decision algorithm that selects a task and calls its next function.

Using this definition, it appears that a task has no reason to know anything about the kernel, and there is therefore no need for a kernel singleton or a pointer from task to kernel. So, for the basic layout, I would consider:

interface ITask {  public bool Done { get; }  public void Step();}class Kernel{  public List<ITask> Tasks;  public void Run() { /* Select a task and run it. */ }}


The issue that appears once subsystems of the game become asynchronously executed tasks is that synchronous communication becomes difficult: you can certainly send a message to another task, but don't expect it to be answered before that other task has a chance to execute. This leads to a certain amount of simplification when the processing happens in a pipelined manner when no answer is expected (for instance, when pushing rendering data into a renderer), but leads to annoying complexity when two-way communication becomes involved (for instance, querying an object for data in an asynchronous world becomes a 'send message, return, receive message or return, process' instead of a plain old function call). It also creates issue with sequential dependencies (such as the world update function having to wait for the render function to be done rendering) but you can design your kernel so that it synchronizes the tasks based on the current frame.

In this approach, your tasks (and subsystems) become servers and function calls become messages. I would suggest a typical architecture for servers: propose a series of messages that can be sent to that server (both synchronous and asynchronous) and group them in an interface. Then, have the server implement both that server and the ITask interface. For example:

interface IScore{  // Asynchronous functions  public void AddToScore(int);  public Result<int> GetScore();  public void Shutdown();}class ScoreBar : IScore, ITask{  int         score;  List<int>   additions;  Result<int> next;  bool        alive;  public AddToScore(int x)   {     this.additions.Add(x);   }  public Result<int> GetScore()  {    if (this.next == null)      this.next = new Result<int>();        return this.next;  }  public void Shutdown()  {    this.alive = false;  }  public void Step()  {      foreach (int add in this.additions)      this.score += add;    this.additions.Clear();        if (this.next != null)      this.next.SetValue(this.score);    this.next = null;  }    public bool Done   {    get { return this.next == null && !this.alive; }  }}


Of course, a score object is a very bad idea for a subsystem (because it will have to be updated anyway, and as such fits better as a synchronous object). But you get the idea. However, of particular interest in this architecture is the ability to dispatch the received messages to another computer or thread, as the asynchronous nature of the system allows for it.

As for the actual connection between the servers (that is, the ability to connect the server which provides Foo to the server which needs Foo), this should ideally be done by the creator of these servers. That is, when you create your Foo-user, you have a reference to the Foo-provider that you give to the Foo-user, thereby granting him the ability to connect to the Foo-provider asynchronously. For instance:

interface IPlayerServer{  public void HasKilledEnemy(int score);}class PlayerServer : IPlayerServer{  IScore scoreServer;  public IScore ScoreServer   {     get { assert (this.scoreServer); return this.scoreServer; }    set { this.scoreServer = value; }  }    void HasKilledEnemy(int score)  {    this.ScoreServer.AddToScore(score);  }}// Somewhere in the subsystem creation functionIScore score = new Score();PlayerServer player = new PlayerServer();player.ScoreServer = score;
Quote:but leads to annoying complexity when two-way communication becomes involved (for instance, querying an object for data in an asynchronous world becomes a 'send message, return, receive message or return, process' instead of a plain old function call). It also creates issue with sequential dependencies (such as the world update function having to wait for the render function to be done rendering) but you can design your kernel so that it synchronizes the tasks based on the current frame.


If you have a global message queue rather than follow an actor model (each subsystem with its own queue) then i think its possible to do this using coroutines/continuations and futures to abstract the round trip messaging. this allows state querying without the slow context switching with reasonable syntactic clarity. it might be possible to flag the sends in priority to avoid the messages getting clogged up at the back of the global queue. ive been a bit stumped on this so i just throw out the idea although i havent implemented.
Quote:Original post by chairthrower
If you have a global message queue rather than follow an actor model (each subsystem with its own queue) then i think its possible to do this using coroutines/continuations and futures to abstract the round trip messaging. this allows state querying without the slow context switching with reasonable syntactic clarity. it might be possible to flag the sends in priority to avoid the messages getting clogged up at the back of the global queue. ive been a bit stumped on this so i just throw out the idea although i havent implemented.


Yes, this would work. However, it's mostly an attempt to patch what is already known to be a design issue: synchronous communication in a server-based architecture is subject to lag. This is one of the main reasons why I don't use a server-based model when I work, and instead use an event-based model.

With events, it becomes elementary to use continuations to solve the asynchronous issue (and it also solves the issue of synchronizing with time-based ticks. The main issue with event-based programming is that it somewhat reduces the isolation of the systems (since now a task can fork as many sub-tasks as necessary to handle asynchronous communication, and therefore a subsystem would be a set of tasks instead of a single one), though a small code layout effort usually helps reducing this.
Thank you, now I understand..

This topic is closed to new replies.

Advertisement