如何在多个脚本的批处理中使用Roslyn C#脚本?

如何在多个脚本的批处理中使用Roslyn C#脚本?,c#,scripting,roslyn,C#,Scripting,Roslyn,我正在编写多线程解决方案,用于将不同来源的数据传输到中央数据库。解决方案一般分为两部分: 单线程导入引擎 在线程中调用导入引擎的多线程客户端 为了最小化定制开发,我使用了Roslyn脚本。此功能在导入引擎项目中的Nuget Package manager中启用。 每次导入都定义为输入表(包含输入字段集合)到目标表(同样包含目标字段集合)的转换 这里使用脚本引擎来允许在输入和输出之间进行自定义转换。对于每个输入/输出对,都有带有自定义脚本的文本字段。以下是用于脚本初始化的简化代码: //Insta

我正在编写多线程解决方案,用于将不同来源的数据传输到中央数据库。解决方案一般分为两部分:

  • 单线程导入引擎
  • 在线程中调用导入引擎的多线程客户端
  • 为了最小化定制开发,我使用了Roslyn脚本。此功能在导入引擎项目中的Nuget Package manager中启用。 每次导入都定义为输入表(包含输入字段集合)到目标表(同样包含目标字段集合)的转换

    这里使用脚本引擎来允许在输入和输出之间进行自定义转换。对于每个输入/输出对,都有带有自定义脚本的文本字段。以下是用于脚本初始化的简化代码:

    //Instance of class passed to script engine
    _ScriptHost = new ScriptHost_Import();
    
    if (Script != "") //Here we have script fetched from DB as text
    {
      try
      {
        //We are creating script object …
        ScriptObject = CSharpScript.Create<string>(Script, globalsType: typeof(ScriptHost_Import));
        //… and we are compiling it upfront to save time since this might be invoked multiple times.
        ScriptObject.Compile();
        IsScriptCompiled = true;
      }
      catch
      {
        IsScriptCompiled = false;
      }
    }
    
    //传递给脚本引擎的类的实例
    _ScriptHost=新ScriptHost_Import();
    if(Script!=“”)//这里我们将脚本作为文本从数据库中获取
    {
    尝试
    {
    //我们正在创建脚本对象…
    ScriptObject=CSharpScript.Create(脚本,globalType:typeof(ScriptHost_Import));
    //…我们正在预先编译它以节省时间,因为它可能会被多次调用。
    ScriptObject.Compile();
    IsScriptCompiled=true;
    }
    抓住
    {
    IsScriptCompiled=false;
    }
    }
    
    稍后,我们将使用以下命令调用此脚本:

    async Task<string> RunScript()
    {
        return (await ScriptObject.RunAsync(_ScriptHost)).ReturnValue.ToString();
    }
    
    异步任务运行脚本()
    {
    return(wait ScriptObject.RunAsync(_ScriptHost)).ReturnValue.ToString();
    }
    
    所以,在导入定义初始化之后,我们可能有任意数量的输入/输出对描述以及脚本对象,在定义脚本的地方,内存足迹每对增加大约50MB。 在将目标行存储到数据库之前,类似的使用模式应用于目标行的验证(每个字段可能有几个脚本用于检查数据的有效性)

    总而言之,具有适度转换/验证脚本的典型内存占用是每个线程200 MB。如果我们需要调用多个线程,内存使用率将非常高,99%将用于编写脚本。 若导入引擎被封装在基于WCF的中间层(我这么做了),我们很快就会遇到“内存不足”的问题

    显而易见的解决方案是使用一个脚本实例,根据需要(输入/输出转换、验证或其他)以某种方式将代码执行分派到脚本中的特定函数。也就是说,我们将使用脚本ID代替每个字段的脚本文本,该脚本ID将作为全局参数传递给脚本引擎。在脚本的某个地方,我们需要切换到将执行并返回适当值的代码的特定部分

    这种解决方案的好处应该是更好地使用内存。缺点:脚本维护从使用它的特定点删除


    在实施此更改之前,我想听听关于此解决方案的意见和对不同方法的建议。

    看起来,为任务使用脚本可能是一种浪费性的过度使用-您使用了许多应用程序层,并且内存已满

    其他解决办法:

    • 如何与数据库接口?您可以根据需要操作查询本身,而不是为此编写整个脚本
    • 使用泛型怎么样?有足够的T以满足您的需求:

      public class ImportEngine

    • 使用(非常类似于使用泛型)

    但是如果您仍然认为脚本是适合您的工具,我发现可以通过在应用程序中运行脚本来降低脚本的内存使用率(而不是使用RunAsync),您可以通过从RunAsync返回逻辑并重新使用它来做到这一点,而不是在繁重且浪费内存的
    RunAsync
    中进行工作。以下是一个例子:

    而不是简单地(脚本字符串):

    您可以这样做(IHaveWork是应用程序中定义的一个接口,只有一种方法
    Work
    ):

    通过这种方式,您只能在短时间内调用重载RunAsync,它返回一个可在应用程序内重复使用的worker(当然,您可以通过向Work方法添加参数和从应用程序继承逻辑等方式对此进行扩展…)

    该模式还打破了应用程序和脚本之间的隔离,因此您可以轻松地从脚本中提供和获取数据

    编辑 一些快速基准:

    此代码:

        static void Main(string[] args)
        {
            Console.WriteLine("Compiling");
            string code = "System.Threading.Thread.SpinWait(100000000);  System.Console.WriteLine(\" Script end\");";
            List<Script<object>> scripts = Enumerable.Range(0, 50).Select(num =>
                 CSharpScript.Create(code, ScriptOptions.Default.WithReferences(typeof(Control).Assembly))).ToList();
    
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // for fair-play
    
            for (int i = 0; i < 10; i++)
                Task.WaitAll(scripts.Select(script => script.RunAsync()).ToArray());
        }
    
    在这个脚本中,我稍微简化了我提出的返回
    Action
    对象的解决方案,但我认为对性能的影响很小(但在实际实现中,我真的认为您应该使用自己的接口使其灵活)


    当脚本运行时,您可以看到内存急剧增加到~240MB,但在我调用垃圾收集器(出于演示目的,我在前面的代码中也这样做了)之后,内存使用量下降到~30MB。它也更快。

    我不确定在创建问题时是否存在这种情况,但有一种非常类似的方法,比如说,如何在不增加程序内存的情况下多次运行脚本。您需要使用CreateDelegate方法,该方法将完全执行预期的操作

    为了方便起见,我将在这里发布:

    var script = CSharpScript.Create<int>("X*Y", globalsType: typeof(Globals));
    ScriptRunner<int> runner = script.CreateDelegate();
    
    for (int i = 0; i < 10; i++)
    {
      Console.WriteLine(await runner(new Globals { X = i, Y = i }));
    }
    
    var script=CSharpScript.Create(“X*Y”,globalsType:typeof(Globals));
    ScriptRunner runner=script.CreateDelegate();
    对于(int i=0;i<10;i++)
    {
    WriteLine(wait runner(新全局变量{X=i,Y=i}));
    }
    

    最初它需要一些内存,但将runner保留在一些全局列表中,稍后快速调用它。

    导入引擎是更大工具集的一小部分。通常,脚本的使用大大增加了这个解决方案的难度。因此,就目前而言,使用Roslyn实现的这种脚本是完美的。就内存消耗而言,我已经注意到了
        static void Main(string[] args)
        {
            Console.WriteLine("Compiling");
            string code = "System.Threading.Thread.SpinWait(100000000);  System.Console.WriteLine(\" Script end\");";
            List<Script<object>> scripts = Enumerable.Range(0, 50).Select(num =>
                 CSharpScript.Create(code, ScriptOptions.Default.WithReferences(typeof(Control).Assembly))).ToList();
    
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // for fair-play
    
            for (int i = 0; i < 10; i++)
                Task.WaitAll(scripts.Select(script => script.RunAsync()).ToArray());
        }
    
        static void Main(string[] args)
        {
            Console.WriteLine("Compiling");
            string code = "return () => { System.Threading.Thread.SpinWait(100000000);  System.Console.WriteLine(\" Script end\"); };";
    
            List<Action> scripts = Enumerable.Range(0, 50).Select(async num =>
                await CSharpScript.EvaluateAsync<Action>(code, ScriptOptions.Default.WithReferences(typeof(Control).Assembly))).Select(t => t.Result).ToList();
    
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
    
            for (int i = 0; i < 10; i++)
                Task.WaitAll(scripts.Select(script => Task.Run(script)).ToArray());
        }
    
    var script = CSharpScript.Create<int>("X*Y", globalsType: typeof(Globals));
    ScriptRunner<int> runner = script.CreateDelegate();
    
    for (int i = 0; i < 10; i++)
    {
      Console.WriteLine(await runner(new Globals { X = i, Y = i }));
    }