zip с исходниками с GitHub · C#, 2013
Исходники на GitHub
Структура на C# для управления процессами. Написана в 2013 году.
Color. Или возьми ColorTemplate<T> и не разводи церемоний.SimpleGraphNode<T>, переопределив Stop и, если нужно, Undo. Если процессу надо делать что-то необычное со входными цветами, наследуй от GraphNode и перепиши Work целиком.OutPoint и SerialOutPoint принимают стартовый цвет в конструкторе.ConnectTo.new, передав в конструктор вершины, чьи итоговые значения тебе нужны. Если передать не первую вершину, все вершины до неё получат сигнал Undo.OnFinish на граф или на любую его вершину, чтобы обработать результаты.Start или StartAsync.Цветной граф — ориентированный ациклический граф, вершины которого — процессы. Каждую вершину и каждое ребро можно покрасить в цвет, и каждой вершине и ребру можно послать сигнал.
Цвет — данные определённого типа, которые ходят между вершинами. Сигнал — данные определённого типа, которые ходят по рёбрам против их направления. Ребро — объект, который соединяет две вершины и имеет положительную длину.
Граф живёт по нескольким правилам:
Из этих аксиом получается несколько видов вершин, которые по-разному обращаются с цветом и сигналами. Когда некоторые из них соединяют друг с другом, лишние рёбра выбрасываются, чтобы не тратить ресурсы.
InPoint)
InPoint): как она красится.Входная точка — вершина с несколькими входящими рёбрами и ровно одним исходящим. Чтобы сэкономить ресурсы и не усложнять логику, исходящее ребро выбрасывается, и входная точка цепляется к следующей вершине напрямую (это её Parent). Как только покрашено любое входящее ребро, входная точка берёт его цвет. Любой сигнал, который получает входная точка, сразу уходит во все входящие рёбра.
OutPoint)
OutPoint): как она красится.У выходной точки одно входящее ребро и несколько исходящих. Как только она покрашена, красятся все исходящие рёбра. Получив последовательный сигнал от одного исходящего ребра, она ждёт, пока просигналят все остальные, и потом посылает AllPosibilitiesFailedException с ошибками каждой ветки в порядке поступления, так что самая первая ошибка стоит в начале списка.
Выходные точки рассылают один цвет сразу в несколько сторон. Прицепи такую к вершине «сохранить в JPEG» и к вершине «сделать бэкап», и один массив фотографий сохранится в JPEG и забэкапится параллельно, в отдельных потоках. Если один и тот же цвет нужен нескольким вершинам, используй одну выходную точку, а не несколько с одинаковыми данными. Только помни, что точка хранит всю цепочку ошибок со всех своих рёбер, так что если нужно следить за ошибками одной конкретной ветки, цепляй её только к этой ветке, ну ты понял.
SerialOutPoint)
SerialOutPoint): как она красится.Последовательная выходная точка красит исходящие рёбра по одному и ждёт, пока каждое отработает. Первым красится самое короткое ребро, оно становится текущим; вершина ждёт его сигнала. По сигналу об ошибке текущим становится самое короткое из непокрашенных рёбер, красится, и ожидание начинается заново. Сигналы от любого ребра, кроме текущего, игнорируются.
Зачем нужны последовательные выходные точки? В основном для цепочек, где заранее неизвестно, какая из нескольких похожих вершин справится с работой. Скажем, надо сконвертировать видео с заданными параметрами, есть несколько конвертеров, каждый поддерживает свои форматы, и заранее не скажешь, какой сработает. Отправь параметры конвертации и имя файла в последовательную выходную точку, к каждому выходу прицепи конвертер, а выходы всех конвертеров сведи в одну входную точку. Последовательная точка запустит первый конвертер; если он упадёт, второй, и так далее, пока какой-нибудь не сконвертирует файл и не отдаст его в общую входную точку. Неиспользованные конвертеры выключаются: их красят в чёрный. Если что-то упадёт дальше по цепочке, последовательная точка вернёт и эту ошибку.
WorkNode)
GraphNode): как он красится.К рабочему узлу прицеплено несколько входных точек и одна выходная. Он преобразует входные цвета по алгоритму, который пишет программист. Узел ждёт, пока покрасятся все входные точки без исключения, преобразует их цвета и красит себя результатом. Если результат получить не удалось, узел посылает сигнал об ошибке и красит себя в чёрный. Любой полученный сигнал он сразу передаёт дальше вверх по дереву. Преобразование, остановка и откат (Work, Stop, Undo) гарантированно выполняются в отдельном потоке.
Чтобы граф запустился через Start или StartAsync, ты называешь его ключевые вершины. Когда все они получили последовательный сигнал, запуск окончен. Пути прослеживаются только для этих вершин, и только они дают результат графа (GraphResult).
Перед запуском все возможные последние вершины и висячие рёбра собираются в список Last. Для каждой из них все стартовые вершины и рёбра, которые могут к ней привести, попадают в список First, а цвета всех первых объектов сохраняются отдельно. Граф готов. У него четыре главных метода: Stop, Clear, Start, Undo.
Stop посылает сигнал Stop во все последние объекты и ждёт, пока он дойдёт до всех первых. Все вершины прекращают работу.Clear вызывается на остановленном графе. Он посылает чёрный во все первые объекты и ждёт, пока чёрный дойдёт до всех последних вершин. Граф полностью очищен, все объекты чёрные.Start вызывается на очищенном графе. Все первые объекты красятся сохранёнными первыми цветами, и граф ждёт, пока просигналят все ключевые объекты, названные при создании.Undo посылает широковещательный Undo во все последние вершины и ждёт, пока он дойдёт до первых. Каждый объект, который реализует IUndoable и не помечен AlreadyUndone, убирает результаты своей работы (временные файлы и тому подобное).Когда все ключевые вершины просигналили, запускается Finish и находит самые длинные пути, которые прошла краска от ключевых объектов. Почему самые длинные? По идее это самая длинная цепочка преобразований без ошибки. Вернёмся к примеру с видео: каждую видеодорожку надо вытащить в отдельный файл, сконвертировать и склеить с остальными. Если по дороге ничего не упало, запуск заканчивается цепочкой «дорожка → файл несконвертированной дорожки → файл сконвертированной дорожки → склеенный файл с несколькими дорожками», и с этой цепочкой можно делать что угодно, например удалить все временные файлы. Если какой-то этап упал, ты получаешь часть цепочки до места падения и можешь, скажем, оставить временные файлы и сказать пользователю, что склейка не удалась, а дорожку вытащить получилось. Если в графе есть выходные точки, длины их рёбер подстраивают длину итогового пути.
После того как самые длинные пути найдены, каждый объект вне их получает сигнал Undo, и вершина, создавшая временный файл, его удаляет. Когда запуск окончен и пути найдены, срабатывает OnFinish.
Цвет — класс, унаследованный от Color. Конечно, это было бы слишком просто, потому что цвета можно смешивать. Color — элемент связного списка, весь список описывает смесь цветов, и любой элемент полностью определяет список. Так что когда объект графа получает цвет, на самом деле он получает смесь, которую можно разобрать через Color.Demix(). Смесь создаётся статическим Color.Mix().
Зачем дурить программиста простым Color, когда ColorMix всё сказал бы сам? Потому что смеси в основном для отладки: можно подмешать отладочную информацию в настоящий результат функции и вытащить её обратно через Demix, а следующая вершина увидит простой цвет без добавок. Если цвет проходит фильтр по типу, вершина принимает его как «хороший» и работает с ним, не замечая примесей. Любой цвет можно считать и одним оттенком, и смесью, смотря что делают функции вершины.
Хорошая практика: использовать смеси только для отладки и заводить отдельный цвет на каждый тип данных, с которым работают функции вершины. Если вершина принимает строку, булево значение и массив чисел без ограничения длины, дай ей два невидимых входа для строки и булева значения, вынесенные в свойства, а саму вершину объяви как принимающую числа. Тогда все числа приходят «хорошими» цветами, а строка и булево значение сидят на невидимых входах.