views:

107

answers:

3

In a system with multiple concurrent tasks operating on data, I want to order the tasks such that there is a minimum amount of waiting time involved. Each task in the system uses a certain set of resources, tasks are issued in a certain order (this order is what I want to calculate), a task will not start until it can get a lock on all required resources. Tasks are issued sequentially, so the 3rd task will not start until the second one has acquired all locks, and so on.

Task 1, Resources [A, B]
Task 2, Resources [B, C]
Task 3, Resources [C, D]
Task 4, Resources [E]

Best Solution
Task 1, [A, B]
Task 3, [C, D] //No waiting is possibly required
Task 4, [E] //Put this before task 3, to maximise the distance between uses of the same resource (minimise chances of lock contention)
Task 2, [B, C] //Some waiting *might* be required here

What algorithm could be used to calculate the optimal ordering such that there is the maximum gap between a resource being used, and then used again?

Nb. This is language agnostic, but bonus points for implementations in C#

A: 

Unless you have clear hierarchy it's going to be difficult to programmatically enforce. For example, you usually have to hold the resources in order to get the next one. IOW to get "B" you must first hold "A". To get "C" you must hold both "A" and "B" and so on. If this is NOT the case then I think the best you can do is to write a common routine that always requests your resources in the same order, A then B then C etc. and route all of your Tasks through this routine. I think ideally you would allocate the resources prior to queue the tasks.

If the resources are all the same, you can use a semaphore with a count of 5. For example a pool of DB connections. This does not seem to be your case though.

No Refunds No Returns
I'm afraid you're not on the right track at all. the resources are allocated prior to the operation even starting, and the users of the resources are too, I calculate this ordering just once. I'm not worried about deadlocks, which is what the first bit of your answer is addressing, I already have ways to deal with deadlocks. Each resource is different, and no two users are allowed to be using the same resource twice.
Martin
+3  A: 

Edit: The objective function given is non-linear, as pointed out by Moron in commmets. Hence this answer can not be used.

One possible approach would be to solve it using linear programming. Here is what I had in mind. Introduce a decision variable T_i_j that is set to 1 if we start task i at time j (I will be counting the tasks from 0 to 3). Furthermore we want to "punish" scheduling tasks close to each other, if they need the same resources. In the example given we want to punish T0_m and T1_n based on the difference between m and n, 3. We can then model the problem as follows

Minimize:
   3 * T0_0 * T1_1 + 2 * T0_0 * T1_2 + 1 * T0_0 * T1_3
 + 3 * T0_1 * T1_2 + 2 * T0_1 * T1_3
 + 3 * T0_2 * T1_3

 + 3 * T1_0 * T2_1 + 2 * T1_0 * T2_2 + 1 * T1_0 * T2_3
 + 3 * T1_1 * T2_2 + 2 * T1_1 * T2_3
 + 3 * T1_2 * T2_3  

Subject to
// We start a task exactly once.
T0_0 + T0_1 + T0_2 + T0_3 = 1
T1_0 + T1_1 + T1_2 + T1_3 = 1
T2_0 + T2_1 + T2_2 + T2_3 = 1
T3_0 + T3_1 + T3_2 + T3_3 = 1

// We can only start a single task at a given time.
T0_0 + T1_0 + T2_0 + T3_0 = 1
T0_1 + T1_1 + T2_1 + T3_1 = 1
T0_2 + T1_2 + T2_2 + T3_2 = 1
T0_3 + T1_3 + T2_3 + T3_3 = 1

We can then use an integer programming solver to find the best combinations to launch the jobs in.

The model above was generated with this (quite awful, but should give you the idea) code

class Program
{
    private static string[][] s_tasks = new string[][]
    {
        new string[] { "A", "B"},
        new string[] { "B", "C"},
        new string[] { "C", "D"},
        new string[] { "E" }
    };

    static void Main(string[] args)
    {
        string filePath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), @"Desktop\lin_prog.txt");
        using (TextWriter writer = new StreamWriter(filePath, false))
        {
            Console.SetOut(writer);
            Console.WriteLine("Given tasks");
            PrintTasks();
            Console.WriteLine();

            Console.WriteLine("Minimize:");
            PrintObjectiveFunction();
            Console.WriteLine();

            Console.WriteLine("Subject to");
            PrintConstraints();
        }
    }

    static void PrintTasks()
    {
        for (int taskCounter = 0; taskCounter < s_tasks.Length; taskCounter++)
        {
            Console.WriteLine("Task {0}: [ {1} ]", taskCounter, String.Join(", ", s_tasks[taskCounter]));
        }
    }

    static void PrintConstraints()
    {
        Console.WriteLine("// We start a task exactly once.");
        for (int taskCounter = 0; taskCounter < s_tasks.Length; taskCounter++)
        for (int timeCounter = 0; timeCounter < s_tasks.Length; timeCounter++)
        {
            Console.Write("T{0}_{1}", taskCounter, timeCounter);
            if (timeCounter != s_tasks.Length - 1)
            {
                Console.Write(" + ");
            }
            else
            {
                Console.WriteLine(" = 1");
            }
        }

        Console.WriteLine();
        Console.WriteLine("// We can only start a single task at a given time.");
        for (int timeCounter = 0; timeCounter < s_tasks.Length; timeCounter++)
        for (int taskCounter = 0; taskCounter < s_tasks.Length; taskCounter++)
        {
            Console.Write("T{0}_{1}", taskCounter, timeCounter);
            if (taskCounter != s_tasks.Length - 1)
            {
                Console.Write(" + ");
            }
            else
            {
                Console.WriteLine(" = 1");
            }
        }

    }

    static void PrintObjectiveFunction()
    {
        StringBuilder objective = new StringBuilder();
        for (int currentTaskCounter = 0; currentTaskCounter < s_tasks.Length; currentTaskCounter++)
        {
            string[] currentTask = s_tasks[currentTaskCounter];
            for (int otherTaskCounter = currentTaskCounter + 1; otherTaskCounter < s_tasks.Length; otherTaskCounter++)
            {
                string[] otherTask = s_tasks[otherTaskCounter];
                if (ShouldPunish(currentTask, otherTask))
                {
                    int maxPunishment = s_tasks.Length;
                    for (int currentTimeCounter = 0; currentTimeCounter < s_tasks.Length; currentTimeCounter++)
                    {
                        string currentTaskDecisionVar = String.Format("T{0}_{1}", currentTaskCounter, currentTimeCounter);
                        for (int otherTimeCounter = currentTimeCounter + 1; otherTimeCounter < s_tasks.Length; otherTimeCounter++)
                        {
                            string otherTaskDecisionVar = String.Format("T{0}_{1}", otherTaskCounter, otherTimeCounter);
                            // Punish tasks more in objective function if they are close in time when launched.
                            int punishment = maxPunishment - (otherTimeCounter - currentTimeCounter);
                            if (0 != objective.Length)
                            {
                                objective.Append(" + ");
                            }

                            objective.AppendFormat
                            (
                                "{0} * {1} * {2}",
                                punishment, currentTaskDecisionVar, otherTaskDecisionVar
                            );
                        }
                        objective.AppendLine();
                    }
                }
            }
        }

        // Nasty hack to align things.
        Console.Write("   " + objective.ToString());
    }

    static bool ShouldPunish(string[] taskOne, string[] taskTwo)
    {
        bool shouldPunish = false;
        foreach (string task in taskOne)
        {
            // We punish tasks iff. they need some of the same resources.
            if(taskTwo.Contains(task))
            {
                shouldPunish = true;
                break;
            }
        }

        return shouldPunish;
    }
}

A couple of things to note

  • The code above runs in something like O(n^5) where n is the number of tasks. That is just to generate the model; integer programming is NP-complete.
  • I'm by no means an OR specialist. I just gave it a try for fun.
  • The solution outlined above uses no inherent constraints that the problem might contain. I could easily imagine a specialized job scheduling algorithm would perform much better (though I still think the problem is NP-complete)
  • If I'm right in the fact, that the problem is NP-complete, then you'll probably be much better off using a cheap heuristic and launch the tasks quickly (unless you can precalculate the solution and use many times).
Mads Ravn
PS. Regarding whether the problem is NP-hard: "For this, I have found a truly wonderful proof, but the comment is too small to contain it." ;)
Mads Ravn
I can precalculate the solution, and use it many many times, in practice calculating the solution is a compile time thing :DI'm going to take a look through this, it looks promising (and complex)
Martin
Well, I hope you can use it. Perhaps, if we are lucky, some OR specialist will read this and be insulted by me not using his or her latest and greatest algorithm, thus forcing them to post a link to an article, where they solved the problem in polynomial time :)
Mads Ravn
How is this an integer programming problem? Integer programming is about integral solutions to _linear_ programming problems. Your function which needs to be minimized is not linear.
Moron
@Moron: You are completely right. I'll update the post to reflect this.
Mads Ravn
so this solution won't work at all?
Martin
@Martin: In the proposed form it will not work - I was so !?$#$ stupid, that I didn't even stop to consider, that the objective function was non-linear. You can find non-linear solvers, but I believe they as a minimum require continous contraints/objective functions. I think it may be possible to change the objective function into something linear, if we punish in the following manner(#tasks - max time difference between conflicting tasks) * Ti_jbut I can not convince myself, that this would actually work in the general case.
Mads Ravn
Ok, well I'm going to accept the graphcolouring solution because I understand how it works, it's simpler, and it actually works ;)
Martin
+1  A: 

I think that if I had a box that solved arbitrary instances of your problem I could feed it disguised graph colouring problems (http://en.wikipedia.org/wiki/Graph_coloring) and have it solve them. I would translate each link into a resource shared by the nodes on either side of the link. All the nodes that are scheduled at the same time can then be coloured the same. So if your problem is easy then Graph Colouring is easy, but Graph Colouring is NP-complete so you're stuffed - well, almost.

OTOH problems like register allocation are reduced to Graph Colouring and approximately solved in practice, so one of the schemes used for Graph Colouring may work for you too. See e.g. http://en.wikipedia.org/wiki/Register_allocation.

mcdowella
Oh, that's an interesting idea!
Martin