【问题标题】:Ordering concurrent tasks to minimise waiting对并发任务进行排序以最大程度地减少等待
【发布时间】:2010-03-03 01:12:38
【问题描述】:

在有多个并发任务对数据进行操作的系统中,我想对任务进行排序,以使所涉及的等待时间最短。 系统中的每个任务使用一定的资源集合,任务按照一定的顺序发出(这个顺序就是我要计算的),一个任务在获得所有所需资源的锁之前不会启动。任务是按顺序发出的,所以在第二个任务获得所有锁之前,第三个任务不会启动,依此类推。

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

可以使用什么算法来计算最佳排序,以使正在使用的资源与再次使用的资源之间存在最大差距?

铌。这与语言无关,但在 C# 中实现的加分项

【问题讨论】:

  • 您需要提供更多信息。除非我们知道任务运行多长时间,否则没有明确的最小等待时间定义。您想尽量减少预期的等待时间吗?然后你需要定义一个任务在时间 j 之后释放锁 i 的概率,等等。
  • 抱歉,您可以假设所有任务都有相似的运行时间,因此减少等待时间涉及最大化资源使用与后续资源使用之间的距离

标签: algorithm language-agnostic concurrency


【解决方案1】:

编辑: 给定的目标函数是非线性的,正如 Moron 在 commmets 中指出的那样。因此,这个答案不能被使用。

一种可能的方法是使用线性规划来解决它。这是我的想法。如果我们在时间 j 开始任务 i,则引入一个设置为 1 的决策变量 T_i_j(我将从 0 到 3 计算任务)。此外,如果它们需要相同的资源,我们希望“惩罚”彼此靠近的调度任务。在给出的示例中,我们希望根据 m 和 n 之间的差值 3 来惩罚 T0_m 和 T1_n。然后我们可以对问题进行如下建模

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

然后我们可以使用integer programming solver 来找到启动作业的最佳组合。

上面的模型是用这个(非常糟糕,但应该给你的想法)代码生成的

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;
    }
}

需要注意的几点

  • 上面的代码在 O(n^5) 中运行,其中 n 是任务数。那只是为了生成模型;整数规划是 NP 完全的。
  • 我绝不是手术室专家。我只是为了好玩而试了一下。
  • 上述解决方案不使用问题可能包含的固有约束。我可以很容易地想象一个专门的作业调度算法会执行得更好(尽管我仍然认为问题是 NP 完全的)
  • 如果我的判断是正确的,即问题是 NP 完全的,那么使用廉价的启发式方法并快速启动任务可能会更好(除非您可以预先计算解决方案并多次使用) .

【讨论】:

  • 附言。关于问题是否为 NP 难:“对此,我找到了一个真正精彩的证明,但评论太小,无法包含它。” ;)
  • 我可以预先计算解决方案,并多次使用它,实际上计算解决方案是编译时的事情:D 我要看看这个,它看起来很有希望(而且很复杂)
  • 嗯,希望你能用。也许,如果我们幸运的话,一些 OR 专家会读到这篇文章并被我侮辱,没有使用他或她的最新和最伟大的算法,从而迫使他们发布一篇文章的链接,他们在多项式时间内解决了问题:)
  • 这是一个整数规划问题吗?整数规划是关于线性规划问题的积分解决方案。您需要最小化的函数不是线性的。
  • @Moron:你完全正确。我会更新帖子以反映这一点。
【解决方案2】:

我认为,如果我有一个可以解决您的问题的任意实例的盒子,我可以提供它伪装的图形着色问题 (http://en.wikipedia.org/wiki/Graph_coloring) 并让它解决它们。我会将每个链接转换为链接两侧的节点共享的资源。然后可以将同时调度的所有节点都涂上相同的颜色。因此,如果您的问题很简单,那么图形着色也很容易,但图形着色是 NP 完全的,所以您已经被填满了——好吧,差不多了。

寄存器分配等OTOH问题被简化为图形着色并在实践中大致解决,因此用于图形着色的方案之一也可能适用于您。参见例如http://en.wikipedia.org/wiki/Register_allocation.

【讨论】:

  • 哦,这是一个有趣的想法!
【解决方案3】:

除非您有明确的层次结构,否则很难以编程方式强制执行。例如,您通常必须持有资源才能获得下一个资源。 IOW 要获得“B”,您必须先持有“A”。要获得“C”,您必须同时持有“A”和“B”等等。如果不是这种情况,那么我认为您能做的最好的事情就是编写一个通用例程,该例程始终以相同的顺序请求您的资源,A 然后 B 然后 C 等等,并通过该例程路由您的所有任务。我认为理想情况下,您会在任务排队之前分配资源。

如果资源都相同,您可以使用计数为 5 的信号量。例如数据库连接池。不过,这似乎不是你的情况。

【讨论】:

  • 恐怕您根本没有走上正轨。资源是在操作开始之前分配的,资源的用户也是,我只计算一次这个排序。我不担心死锁,这是您回答的第一部分要解决的问题,我已经有了处理死锁的方法。每个资源都是不同的,不允许两个用户重复使用同一个资源。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-27
相关资源
最近更新 更多