Colorgraph

структура на C# для управления процессами

Скачать

zip с исходниками с GitHub · C#, 2013
Исходники на GitHub

Структура на C# для управления процессами. Написана в 2013 году.

С чего начать

  1. Разберись, какие типы данных входят в твои процессы и выходят из них. Заведи на каждый тип свой цвет, унаследованный от Color. Или возьми ColorTemplate<T> и не разводи церемоний.
  2. Напиши все процессы своего графа. Лучше всего наследовать их от SimpleGraphNode<T>, переопределив Stop и, если нужно, Undo. Если процессу надо делать что-то необычное со входными цветами, наследуй от GraphNode и перепиши Work целиком.
  3. Создай первые вершины графа и дай им стартовый цвет. OutPoint и SerialOutPoint принимают стартовый цвет в конструкторе.
  4. Собери граф из этих первых вершин с помощью ConnectTo.
  5. Создай граф через new, передав в конструктор вершины, чьи итоговые значения тебе нужны. Если передать не первую вершину, все вершины до неё получат сигнал Undo.
  6. Повесь обработчики OnFinish на граф или на любую его вершину, чтобы обработать результаты.
  7. Запусти через Start или StartAsync.

Подробнее

Термины

Цветной граф — ориентированный ациклический граф, вершины которого — процессы. Каждую вершину и каждое ребро можно покрасить в цвет, и каждой вершине и ребру можно послать сигнал.

Цвет — данные определённого типа, которые ходят между вершинами. Сигнал — данные определённого типа, которые ходят по рёбрам против их направления. Ребро — объект, который соединяет две вершины и имеет положительную длину.

Аксиомы

Граф живёт по нескольким правилам:

  1. Любой объект (вершину или ребро) можно покрасить.
  2. Перекрашивать уже покрашенный объект можно только в чёрный.
  3. Вершина сама решает, какой цвет принять и при каком условии. Гарантии только две: если покрашены все входящие рёбра, покрашена и вершина; если все входящие рёбра чёрные, вершина чёрная.
  4. Любому объекту можно послать сигнал против направления графа (от конца к началу).
  5. Сигналы бывают двух видов: последовательные и широковещательные.
  6. Объект принимает только один последовательный сигнал, остальные игнорирует.
  7. Вершина сама решает, как и когда обрабатывать последовательный сигнал. Гарантия одна: если просигналили все исходящие рёбра, вершина сигналит тоже.
  8. Широковещательный сигнал вершина сразу передаёт дальше вверх по дереву. Вершина, которая уже видела широковещательный сигнал с данным номером, потом его игнорирует.

Вершины

Из этих аксиом получается несколько видов вершин, которые по-разному обращаются с цветом и сигналами. Когда некоторые из них соединяют друг с другом, лишние рёбра выбрасываются, чтобы не тратить ресурсы.

Входная точка (InPoint)

Входная точка: как она красится
Входная точка (InPoint): как она красится.

Входная точка — вершина с несколькими входящими рёбрами и ровно одним исходящим. Чтобы сэкономить ресурсы и не усложнять логику, исходящее ребро выбрасывается, и входная точка цепляется к следующей вершине напрямую (это её Parent). Как только покрашено любое входящее ребро, входная точка берёт его цвет. Любой сигнал, который получает входная точка, сразу уходит во все входящие рёбра.

Выходная точка (OutPoint)

Выходная точка: как она красится
Выходная точка (OutPoint): как она красится.

У выходной точки одно входящее ребро и несколько исходящих. Как только она покрашена, красятся все исходящие рёбра. Получив последовательный сигнал от одного исходящего ребра, она ждёт, пока просигналят все остальные, и потом посылает AllPosibilitiesFailedException с ошибками каждой ветки в порядке поступления, так что самая первая ошибка стоит в начале списка.

Выходные точки рассылают один цвет сразу в несколько сторон. Прицепи такую к вершине «сохранить в JPEG» и к вершине «сделать бэкап», и один массив фотографий сохранится в JPEG и забэкапится параллельно, в отдельных потоках. Если один и тот же цвет нужен нескольким вершинам, используй одну выходную точку, а не несколько с одинаковыми данными. Только помни, что точка хранит всю цепочку ошибок со всех своих рёбер, так что если нужно следить за ошибками одной конкретной ветки, цепляй её только к этой ветке, ну ты понял.

Последовательная выходная точка (SerialOutPoint)

Последовательная выходная точка: как она красится
Последовательная выходная точка (SerialOutPoint): как она красится.

Последовательная выходная точка красит исходящие рёбра по одному и ждёт, пока каждое отработает. Первым красится самое короткое ребро, оно становится текущим; вершина ждёт его сигнала. По сигналу об ошибке текущим становится самое короткое из непокрашенных рёбер, красится, и ожидание начинается заново. Сигналы от любого ребра, кроме текущего, игнорируются.

Зачем нужны последовательные выходные точки? В основном для цепочек, где заранее неизвестно, какая из нескольких похожих вершин справится с работой. Скажем, надо сконвертировать видео с заданными параметрами, есть несколько конвертеров, каждый поддерживает свои форматы, и заранее не скажешь, какой сработает. Отправь параметры конвертации и имя файла в последовательную выходную точку, к каждому выходу прицепи конвертер, а выходы всех конвертеров сведи в одну входную точку. Последовательная точка запустит первый конвертер; если он упадёт, второй, и так далее, пока какой-нибудь не сконвертирует файл и не отдаст его в общую входную точку. Неиспользованные конвертеры выключаются: их красят в чёрный. Если что-то упадёт дальше по цепочке, последовательная точка вернёт и эту ошибку.

Рабочий узел (WorkNode)

Рабочий узел: как он красится
Рабочий узел (GraphNode): как он красится.

К рабочему узлу прицеплено несколько входных точек и одна выходная. Он преобразует входные цвета по алгоритму, который пишет программист. Узел ждёт, пока покрасятся все входные точки без исключения, преобразует их цвета и красит себя результатом. Если результат получить не удалось, узел посылает сигнал об ошибке и красит себя в чёрный. Любой полученный сигнал он сразу передаёт дальше вверх по дереву. Преобразование, остановка и откат (Work, Stop, Undo) гарантированно выполняются в отдельном потоке.

Как это работает

Чтобы граф запустился через Start или StartAsync, ты называешь его ключевые вершины. Когда все они получили последовательный сигнал, запуск окончен. Пути прослеживаются только для этих вершин, и только они дают результат графа (GraphResult).

Перед запуском все возможные последние вершины и висячие рёбра собираются в список Last. Для каждой из них все стартовые вершины и рёбра, которые могут к ней привести, попадают в список First, а цвета всех первых объектов сохраняются отдельно. Граф готов. У него четыре главных метода: Stop, Clear, Start, Undo.

Когда все ключевые вершины просигналили, запускается Finish и находит самые длинные пути, которые прошла краска от ключевых объектов. Почему самые длинные? По идее это самая длинная цепочка преобразований без ошибки. Вернёмся к примеру с видео: каждую видеодорожку надо вытащить в отдельный файл, сконвертировать и склеить с остальными. Если по дороге ничего не упало, запуск заканчивается цепочкой «дорожка → файл несконвертированной дорожки → файл сконвертированной дорожки → склеенный файл с несколькими дорожками», и с этой цепочкой можно делать что угодно, например удалить все временные файлы. Если какой-то этап упал, ты получаешь часть цепочки до места падения и можешь, скажем, оставить временные файлы и сказать пользователю, что склейка не удалась, а дорожку вытащить получилось. Если в графе есть выходные точки, длины их рёбер подстраивают длину итогового пути.

После того как самые длинные пути найдены, каждый объект вне их получает сигнал Undo, и вершина, создавшая временный файл, его удаляет. Когда запуск окончен и пути найдены, срабатывает OnFinish.

О цветах

Цвет — класс, унаследованный от Color. Конечно, это было бы слишком просто, потому что цвета можно смешивать. Color — элемент связного списка, весь список описывает смесь цветов, и любой элемент полностью определяет список. Так что когда объект графа получает цвет, на самом деле он получает смесь, которую можно разобрать через Color.Demix(). Смесь создаётся статическим Color.Mix().

Зачем дурить программиста простым Color, когда ColorMix всё сказал бы сам? Потому что смеси в основном для отладки: можно подмешать отладочную информацию в настоящий результат функции и вытащить её обратно через Demix, а следующая вершина увидит простой цвет без добавок. Если цвет проходит фильтр по типу, вершина принимает его как «хороший» и работает с ним, не замечая примесей. Любой цвет можно считать и одним оттенком, и смесью, смотря что делают функции вершины.

Хорошая практика: использовать смеси только для отладки и заводить отдельный цвет на каждый тип данных, с которым работают функции вершины. Если вершина принимает строку, булево значение и массив чисел без ограничения длины, дай ей два невидимых входа для строки и булева значения, вынесенные в свойства, а саму вершину объяви как принимающую числа. Тогда все числа приходят «хорошими» цветами, а строка и булево значение сидят на невидимых входах.