C#Multithreaded (16): Teach you a workflow by hand

Keywords: github

Catalog

Preface

I have learned a lot about the basics of multi-threading and tasks before, so I'll try them out here.In this tutorial, you can write a simple workflow engine.

This tutorial is task-based and can be easily completed by reading three articles on asynchronism and mastering the C#basics.

Due to the workflow program written in this article, mainly using tasks, some logical processes will be more difficult to understand, just test more.The code is mainly C#based, why is it simple?

  • Does not contain async, await
  • Almost no multi-threading (with a read-write lock)
  • Does not contain an expression tree
  • Almost no reflection (there's a small area to reflect on, but it's very simple)
  • No complex algorithm

Because it is task-based, it is easy to design composite processes to form complex workflows.

Since it's only about the basics, it doesn't include many kinds of process control, just a few simple ones.

First, don't use it in business.This workflow is very simple, just a few functions, and is based on the knowledge points of the author's multi-threaded series of articles.This is written to explain the operation of the task and to give the reader a deeper understanding of the task.

Code address: https://github.com/whuanle/CZGL.FLow

These two days are busy moving things, I didn't write articles carefully today, where the code is not clear, you can find me in the WeChat group.WeChat Name: Craftsmanship, dotnet group I am basically in.

node

Before we get started, let's design several process controls.

A step/process/node is called a step.

Then

A common node containing a task.

Multiple Then nodes can form a continuous workflow.

Parallel

Parallel Node, where you can set up multiple parallel nodes to put in Parallel and create new branches for any one node.

Schedule

Timing node, which executes tasks in the node after a certain time.

Delay

Block the current task for a while.

Try it out

Sequential Node

Open your VS, create a project, Nuget references CZGL.DoFlow, version 1.0.2.

Create a class MyFlow1 that inherits IDoFlow.

    public class MyFlow1 : IDoFlow
    {
        public int Id => 1;

        public string Name => "Give yourself a name";

        public int Version => 1;

        public IDoFlowBuilder Build(IDoFlowBuilder builder)
        {
            throw new NotImplementedException();
        }
    }

You can create multiple workflow tasks, and the Id of each workflow must be unique.Name and Version are optional, as the authors do not have logic for these fields here.

IDoFlowBuilder is an interface for building workflows.

Let's write a workflow test.

/// <summary>
///Normal Node Then Usage
/// </summary>
public class MyFlow1 : IDoFlow
{
    public int Id => 1;
    public string Name => "test";
    public int Version => 1;

    public IDoFlowBuilder Build(IDoFlowBuilder builder)
    {
        builder.StartWith(() =>
        {
            Console.WriteLine("Workflow Start");
        }).Then(() =>
        {
            Console.WriteLine("Next Node");
        }).Then(() =>
         {
             Console.WriteLine("Last Node");
         });
        return builder;
    }
} 

Main method:

        static void Main(string[] args)
        {
            FlowCore.RegisterWorkflow<MyFlow1>();
            // FlowCore.RegisterWorkflow(new MyFlow1());
            FlowCore.Start(1);
            Console.ReadKey();
        }

The.StartWith() method starts a workflow;

FlowCore.RegisterWorkflow <T>() Register a workflow;

FlowCore.Start(); execute a workflow;

Parallel Tasks

The code is as follows:

    /// <summary>
    ///Parallel Node Usage
    /// </summary>
    public class MyFlow2 : IDoFlow
    {
        public int Id => 2;
        public string Name => "test";
        public int Version => 1;

        public IDoFlowBuilder Build(IDoFlowBuilder builder)
        {
            builder.StartWith()
                .Parallel(steps =>
                {
                    // Each parallel task can also be designed to continue other tasks later
                    steps.Do(() =>
                    {
                        Console.WriteLine("Parallel 1");
                    }).Do(() =>
                    {
                        Console.WriteLine("Parallel 2");
                    });
                    steps.Do(() =>
                    {
                        Console.WriteLine("Parallel 3");
                    });

                    // This method must be called when parallel task design is complete
                    // This method must be placed at the end of all parallel tasks.Do()
                    steps.EndParallel();

                    // If.Do() is after EndParallel(), then it will not wait for this task
                    steps.Do(() => { Console.WriteLine("Parallel Asynchronous"); });

                    // Open a new branch
                    steps.StartWith()
                    .Then(() =>
                    {
                        Console.WriteLine("New Branch" + Task.CurrentId);
                    }).Then(() => { Console.WriteLine("Branch 2.0" + Task.CurrentId); });

                }, false)
                .Then(() =>
                {
                    Console.WriteLine("11111111111111111 ");
                });

            return builder;
        }
    }

Main method:

        static void Main(string[] args)
        {
            FlowCore.RegisterWorkflow<MyFlow2>();
            FlowCore.Start(2);
            Console.ReadKey();
        }

From the examples above, you can get a general idea of the program we are going to write in this article.

Write Workflow

Create a class library project called DoFlow.

Create three directories: Extensions, Interfaces, Services.

Interface Builder

Create a new IStepBuilder interface file to the Interfaces directory, which reads as follows:

using System;

namespace DoFlow.Interfaces
{
    public interface IStepBuilder
    {
        /// <summary>
        ///Normal Node
        /// </summary>
        /// <param name="stepBuilder"></param>
        /// <returns></returns>
        IStepBuilder Then(Action action);

        /// <summary>
        ///multiple nodes
        /// <para>By default, all tasks need to be completed before this step is complete </para>
        /// </summary>
        /// <param name="action"></param>
        /// <param name="anyWait">Any task can be completed and jumped to the next step</param>
        /// <returns></returns>
        IStepBuilder Parallel(Action<IStepParallel> action, bool anyWait = false);

        /// <summary>
        ///Node will execute after a certain time interval
        /// <para>Asynchronous, does not block the current workflow, scheduled tasks will trigger </para>after a period of time
        /// </summary>
        /// <returns></returns>
        IStepBuilder Schedule(Action action, TimeSpan time);

        /// <summary>
        ///Block for a while
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        IStepBuilder Delay(TimeSpan time);
    }
}

Create a new IStepParallel file to the Interfaces directory.

using System;

namespace DoFlow.Interfaces
{
    /// <summary>
    ///Parallel Tasks
    /// <para>By default, this node is not complete until all parallel tasks for this node have been completed </para>
    /// </summary>
    public interface IStepParallel
    {
        /// <summary>
        ///a parallel task
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        IStepParallel Do(Action action);

        /// <summary>
        ///Start a branch
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        IStepBuilder StartWith(Action action = null);

        /// <summary>
        ///This method must be used to end a parallel task
        /// </summary>
        void EndParallel();
    }

    /// <summary>
    ///Parallel Tasks
    /// <para>Once any task is completed, you can jump to the next step</para>
    /// </summary>
    public interface IStepParallelAny : IStepParallel
    {

    }
}

Workflow Builder

Create a new IDoFlowBuilder interface file to the Interfaces directory.

using System;
using System.Threading.Tasks;

namespace DoFlow.Interfaces
{
    /// <summary>
    ///Build Workflow Tasks
    /// </summary>
    public interface IDoFlowBuilder
    {
        /// <summary>
        ///Start a step
        /// </summary>
        IStepBuilder StartWith(Action action = null);
        void EndWith(Action action);

        Task ThatTask { get; }
    }
}

Create a new IDoFlow interface file to the Interfaces directory.

namespace DoFlow.Interfaces
{

    /// <summary>
    ///Workflow
    /// <para>parameterless delivery </para>
    /// </summary>
    public interface IDoFlow
    {
        /// <summary>
        ///Global Unique Identification
        /// </summary>
        int Id { get; }

        /// <summary>
        ///Identify the name of this workflow
        /// </summary>
        string Name { get; }

        /// <summary>
        ///Identify the version of this workflow
        /// </summary>
        int Version { get; }

        IDoFlowBuilder Build(IDoFlowBuilder builder);
    }
}

Dependent Injection

Create a new DependencyInjectionService file to the Services directory.

Used to implement dependent injection and decoupling.

using DoFlow.Extensions;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace DoFlow.Services
{
    /// <summary>
    ///Dependent Injection Service
    /// </summary>
    public static class DependencyInjectionService
    {
        private static IServiceCollection _servicesList;
        private static IServiceProvider _services;
        static DependencyInjectionService()
        {
            IServiceCollection services = new ServiceCollection();
            _servicesList = services;
            // Injection Engine Required Services
            InitExtension.StartInitExtension();
            var serviceProvider = services.BuildServiceProvider();
            _services = serviceProvider;
        }

        /// <summary>
        ///Add an injection into container service
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TImplementation"></typeparam>
        public static void AddService<TService, TImplementation>()
            where TService : class
            where TImplementation : class, TService
        {
            _servicesList.AddTransient<TService, TImplementation>();
        }

        /// <summary>
        ///Get the services you need
        /// </summary>
        /// <typeparam name="TIResult"></typeparam>
        /// <returns></returns>
        public static TIResult GetService<TIResult>()
        {
            TIResult Tservice = _services.GetService<TIResult>();
            return Tservice;
        }
    }
}

Add an InitExtension file to the Extensions directory.

using DoFlow.Interfaces;
using DoFlow.Services;

namespace DoFlow.Extensions
{
    public static class InitExtension
    {
        private static bool IsInit = false;
        public static void StartInitExtension()
        {
            if (IsInit) return;
            IsInit = true;
            DependencyInjectionService.AddService<IStepBuilder, StepBuilder>();
            DependencyInjectionService.AddService<IDoFlowBuilder, DoFlowBuilder>();
            DependencyInjectionService.AddService<IStepParallel, StepParallelWhenAll>();
            DependencyInjectionService.AddService<IStepParallelAny, StepParallelWhenAny>();
        }
    }
}

Implement workflow resolution

The following files are all created in the Services directory.

Create a new StepBuilder file to parse nodes and build tasks.

using DoFlow.Interfaces;
using System;
using System.Threading.Tasks;

namespace DoFlow.Services
{

    /// <summary>
    ///Node Work Engine
    /// </summary>
    public class StepBuilder : IStepBuilder
    {
        private Task _task;

        /// <summary>
        ///Delayed execution
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        public IStepBuilder Delay(TimeSpan time)
        {
            Task.Delay(time).Wait();
            return this;
        }

        /// <summary>
        ///parallel step
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        public IStepBuilder Parallel(Action<IStepParallel> action, bool anyAwait = false)
        {
            IStepParallel parallel = anyAwait ? DependencyInjectionService.GetService<IStepParallelAny>() : DependencyInjectionService.GetService<IStepParallel>();
            Task task = new Task(() =>
            {
                action.Invoke(parallel);
            });

            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
            {
                task.Start();
            });
            _task = task;
            return this;
        }

        /// <summary>
        ///Schedule Tasks
        /// </summary>
        /// <param name="action"></param>
        /// <param name="time"></param>
        /// <returns></returns>
        public IStepBuilder Schedule(Action action, TimeSpan time)
        {
            Task.Factory.StartNew(() =>
            {
                Task.Delay(time).Wait();
                action.Invoke();
            });
            return this;
        }

        /// <summary>
        ///normal step
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        public IStepBuilder Then(Action action)
        {
            Task task = new Task(action);
            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
            {
                task.Start();
                task.Wait();
            });
            _task = task;
            return this;
        }

        public void SetTask(Task task)
        {
            _task = task;
        }
    }
}

Create a new StepParallel file with two classes for synchronization tasks.

using DoFlow.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace DoFlow.Services
{
    /// <summary>
    ///Layer 1 All tasks are completed before jumping to the next step
    /// </summary>
    public class StepParallelWhenAll : IStepParallel
    {
        private Task _task;
        private readonly List<Task> _tasks = new List<Task>();
        public StepParallelWhenAll()
        {
            _task = new Task(() => { },TaskCreationOptions.AttachedToParent);
        }
        public IStepParallel Do(Action action)
        {
            _tasks.Add(Task.Run(action));
            return this;
        }

        public void EndParallel()
        {
            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
            {
                Task.WhenAll(_tasks).Wait();
            });
        }

        public IStepBuilder StartWith(Action action = null)
        {
            Task task =
                action is null ? new Task(() => { })
                : new Task(action);
            var _stepBuilder = DependencyInjectionService.GetService<IStepBuilder>();
            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => { task.Start(); });

            return _stepBuilder;
        }
    }

    /// <summary>
    ///Jump to the next step after completing any task
    /// </summary>
    public class StepParallelWhenAny : IStepParallelAny
    {
        private Task _task;
        private readonly List<Task> _tasks = new List<Task>();
        public StepParallelWhenAny()
        {
            _task = Task.Run(() => { });
        }
        public IStepParallel Do(Action action)
        {
            _tasks.Add(Task.Run(action));
            return this;
        }

        public void EndParallel()
        {
            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
            {
                Task.WhenAny(_tasks).Wait();
            });
        }

        public IStepBuilder StartWith(Action action = null)
        {
            Task task =
                action is null ? new Task(() => { })
                : new Task(action);
            var _stepBuilder = DependencyInjectionService.GetService<IStepBuilder>();
            _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => { task.Start(); });

            return _stepBuilder;
        }
    }
}

Create a new DoFlowBuilder file for building workflows.

using DoFlow.Interfaces;
using System;
using System.Threading.Tasks;

namespace DoFlow.Services
{
    public class DoFlowBuilder : IDoFlowBuilder
    {
        private Task _task;
        public Task ThatTask => _task;

        public void EndWith(Action action)
        {
            _task.Start();
        }

        public IStepBuilder StartWith(Action action = null)
        {
            if (action is null)
                _task = new Task(() => { });
            else _task = new Task(action);

            IStepBuilder _stepBuilder = DependencyInjectionService.GetService<IStepBuilder>();
            ((StepBuilder)_stepBuilder).SetTask(_task);
            return _stepBuilder;
        }
    }
}

Create a new FlowEngine file to execute the workflow.

using DoFlow.Interfaces;

namespace DoFlow.Services
{
    /// <summary>
    ///Workflow Engine
    /// </summary>
    public class FlowEngine
    {
        private readonly IDoFlow _flow;
        public FlowEngine(IDoFlow flow)
        {
            _flow = flow;
        }

        /// <summary>
        ///Start a workflow
        /// </summary>
        public void Start()
        {
            IDoFlowBuilder builder = DependencyInjectionService.GetService<IDoFlowBuilder>();
            _flow.Build(builder).ThatTask.Start();
        }
    }
}

Create a new FlowCore file to store and index workflows.Use read-write locks to solve concurrent dictionary problems.

using DoFlow.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading;

namespace DoFlow.Services
{
    public static class FlowCore
    {
        private static Dictionary<int, FlowEngine> flowEngines = new Dictionary<int, FlowEngine>();

        // Read-write lock
        private static ReaderWriterLockSlim readerWriterLockSlim = new ReaderWriterLockSlim();

        /// <summary>
        ///Register Workflow
        /// </summary>
        /// <param name="flow"></param>
        public static bool RegisterWorkflow(IDoFlow flow)
        {
            try
            {
                readerWriterLockSlim.EnterReadLock();
                if (flowEngines.ContainsKey(flow.Id))
                    return false;
                flowEngines.Add(flow.Id, new FlowEngine(flow));
                return true;
            }
            finally
            {
                readerWriterLockSlim.ExitReadLock();
            }
        }

        /// <summary>
        ///Register Workflow
        /// </summary>
        /// <param name="flow"></param>
        public static bool RegisterWorkflow<TDoFlow>()
        {

            Type type = typeof(TDoFlow);
            IDoFlow flow = (IDoFlow)Activator.CreateInstance(type);
            try
            {
                readerWriterLockSlim.EnterReadLock();
                if (flowEngines.ContainsKey(flow.Id))
                    return false;
                flowEngines.Add(flow.Id, new FlowEngine(flow));
                return true;
            }
            finally
            {
                readerWriterLockSlim.ExitReadLock();
            }
        }

        /// <summary>
        ///Workflow to start
        /// </summary>
        /// <param name="id"></param>
        public static bool Start(int id)
        {
            FlowEngine engine;
            // Read-write lock
            try
            {
                readerWriterLockSlim.EnterUpgradeableReadLock();

                if (!flowEngines.ContainsKey(id))
                    return default;
                try
                {
                    readerWriterLockSlim.EnterWriteLock();
                    engine = flowEngines[id];
                }
                catch { return default; }
                finally
                {
                    readerWriterLockSlim.ExitWriteLock();
                }
            }
            catch { return default; }
            finally
            {
                readerWriterLockSlim.ExitUpgradeableReadLock();
            }

            engine.Start();
            return true;
        }
    }
}

So the program is finished.

Busy.

Posted by niesom on Sun, 03 May 2020 17:32:14 -0700