Оптимизированный алгоритм для планирования задач с зависимостью?

Есть задачи, которые читаются из файла, выполняют некоторую обработку и записывают в файл. Эти задачи должны планироваться на основе зависимости. Также задачи могут выполняться параллельно, поэтому алгоритм должен быть оптимизирован для запуска зависимых задач в последовательном и максимально возможном параллельном режиме.

например:

  • A → B
  • A → C
  • B → D
  • E → F

Таким образом, будет запущен один из способов запуска этого 1, 2 и 4 параллельно. Далее следуют 3.

Другим способом может быть запустите 1, а затем параллельно выполните 2, 3 и 4.

Другой может быть запущен 1 и 3 в последовательном порядке, 2 и 4. Параллельно.

Любые идеи?

Ответы

Ответ 1

Пусть каждая задача (например, A,B,...) является узлами в направленном ациклическом графе и определяет дуги между узлами на основе 1,2,....

http://en.wikipedia.org/wiki/Topological_sorting

Затем вы можете упорядочить ваш график (или использовать метод, основанный на поиске, например BFS). В вашем примере C<-A->B->D и E->F, поэтому A и E имеют глубину 0 и должны быть запущены первыми. Затем вы можете запускать F, B и C параллельно, а затем D.

Кроме того, посмотрите PERT.

Обновление:

Откуда вы знаете, имеет ли B более высокий приоритет, чем F?

Это интуиция, лежащая в основе топологической сортировки, используемой для поиска порядка.

Сначала он находит узлы корня (без входящих ребер) (так как он должен существовать в DAG). В вашем случае это A и E. Это устанавливает первый раунд рабочих мест, которые должны быть завершены. Затем должны быть завершены дочерние узлы корня (B, C и F). Это легко получить, запросив ваш график. Затем процесс повторяется до тех пор, пока не будет найдено узлов (заданий) (завершено).

Ответ 2

Учитывая сопоставление элементов и элементов, на которые они зависят, топологические позиции сортировки сортируются так, что ни один элемент не предшествует элементу, от которого он зависит.

Эта задача кода Rosetta имеет решение в Python который может сообщить вам, какие элементы доступны для обработки параллельно.

Учитывая ваш ввод, код становится:

try:
    from functools import reduce
except:
    pass

data = { # From: http://stackoverflow.com/questions/18314250/optimized-algorithm-to-schedule-tasks-with-dependency
    # This   <-   This  (Reverse of how shown in question)
    'B':         set(['A']),
    'C':         set(['A']),
    'D':         set(['B']),
    'F':         set(['E']),
    }

def toposort2(data):
    for k, v in data.items():
        v.discard(k) # Ignore self dependencies
    extra_items_in_deps = reduce(set.union, data.values()) - set(data.keys())
    data.update({item:set() for item in extra_items_in_deps})
    while True:
        ordered = set(item for item,dep in data.items() if not dep)
        if not ordered:
            break
        yield ' '.join(sorted(ordered))
        data = {item: (dep - ordered) for item,dep in data.items()
                if item not in ordered}
    assert not data, "A cyclic dependency exists amongst %r" % data

print ('\n'.join( toposort2(data) ))

Что затем генерирует этот вывод:

A E
B C F
D

Элементы в одной строке вывода могут обрабатываться в любом подряде или, действительно, параллельно; просто до тех пор, пока все элементы более высокой строки обрабатываются перед элементами следующих строк, чтобы сохранить зависимости.

Ответ 3

Ваши задачи - это ориентированный граф с (надеюсь) без циклов.

I содержит sources и wells (источники - это задачи, которые не зависят (не имеют входящего края), а скважины - это задачи, которые не разблокируют задачу (нет исходящего края)).

Простым решением было бы отдать приоритет вашим задачам, основываясь на их полезности (назовем это U.

Как правило, начиная с колодцев, они имеют полезность U = 1, потому что мы хотим, чтобы они закончили.

Поместите все предшественники колодцев в список L, который в настоящее время оценивается node.

Затем, беря каждый node в L, значение U представляет собой сумму значений U для узлов, которые зависят от него + 1. Поместите всех родителей текущего node в L список.

Петля, пока все узлы не будут обработаны.

Затем запустите задачу, которая может быть запущена, и имеет самое большое значение U, потому что именно она разблокирует наибольшее количество задач.

В вашем примере

U(C) = U(D) = U(F) = 1
U(B) = U(E) = 2
U(A) = 4

Считая, что вы начнете сначала с E, если возможно, тогда B и C (если возможно), то D и F

Ответ 4

сначала генерируют топологическое упорядочение ваших задач. проверьте циклы на этом этапе. после этого вы можете использовать parallelism, глядя на максимальные антицехи. грубо говоря, это наборы задач без зависимостей между их элементами.

для теоретической перспективы, этот документ охватывает эту тему.

Ответ 5

Без учета последовательного/параллельного аспекта проблемы этот код может по крайней мере определить общее серийное решение:

def order_tasks(num_tasks, task_pair_list):
    task_deps= []
    #initialize the list
    for i in range(0, num_tasks):
        task_deps[i] = {}

    #store the dependencies
    for pair in task_pair_list:
        task = pair.task
        dep = pair.dependency

        task_deps[task].update({dep:1})

    #loop through list to determine order
    while(len(task_pair_list) > 0):
        delete_task = None

        #find a task with no dependencies
        for task in task_deps:
            if len(task_deps[task]) == 0:
                delete_task = task
                print task
                task_deps.pop(task)
                break

        if delete_task == None:
            return -1

        #check each task hash of dependencies for delete_task
        for task in task_deps:
            if delete_key in task_deps[task]:
                del task_deps[task][delete_key]

    return 0

Если вы обновляете цикл, который проверяет соответствие, которое было полностью удовлетворено, чтобы перебирать весь список и выполнять/удалять задачи, у которых больше нет каких-либо зависимостей одновременно, это также должно позволить вам воспользоваться дополнением задачи параллельно.