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

时间:2017-04-16 01:50:46

标签: c# scripting roslyn

我正在编写多线程解决方案,用于将数据从不同的源传输到中央数据库。解决方案通常包含两部分:

  1. 单线程导入引擎
  2. 在线程中调用导入引擎的多线程客户端。
  3. 为了最大限度地减少自定义开发,我使用的是Roslyn脚本。导入引擎项目中的Nuget包管理器启用了此功能。 每个导入都被定义为输入表的转换 - 具有输入字段的集合 - 再次转换为目标表 - 再次使用目标字段集合。

    此处使用脚本引擎允许输入和输出之间的自定义转换。对于每个输入/输出对,都有包含自定义脚本的文本字段。以下是用于脚本初始化的简化代码:

    //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;
      }
    }
    

    稍后我们将使用以下命令调用此脚本:

    async Task<string> RunScript()
    {
        return (await ScriptObject.RunAsync(_ScriptHost)).ReturnValue.ToString();
    }
    

    因此,在导入定义初始化之后,我们可能会有任意数量的输入/输出对描述以及脚本对象,在定义脚本的情况下,每对内存占用量增加大约50 MB。 在将目标行存储到数据库之前,类似的使用模式将应用于目标行的验证(每个字段可能有多个用于检查数据有效性的脚本)。

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

    明显的解决方案是让一个脚本实例以某种方式将代码执行分配给脚本内的特定函数,具体取决于需要(输入/输出转换,验证或其他)。即我们将使用SCRIPT_ID代替每个字段的脚本文本,该SCRIPT_ID将作为全局参数传递给脚本引擎。在脚本的某处,我们需要切换到将执行并返回适当值的特定代码部分。

    此类解决方案的好处应该是更好的内存使用率。贬低脚本维护从使用它的特定点移除的事实。

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

2 个答案:

答案 0 :(得分:3)

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

其他解决方案:

  • 您如何与数据库交互?您可以根据需要操作查询本身,而不是为此编写完整的脚本。
  • 如何使用Generics?有足够的T来满足您的需求:

    public class ImportEngine<T1,T2,T3,T3,T5>

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

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

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

DoSomeWork();

您可以这样做(IHaveWork是您应用中定义的界面,只有一种方法Work):

public class ScriptWork : IHaveWork
{
    Work()
    {
        DoSomeWork();
    }
}
return new ScriptWork();

这样你只能在短时间内调用繁重的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());
    }

在我的环境中消耗大约600MB(仅在ScriptOption中引用了System.Windows.Form来调整脚本大小)。 它重用Script<object> - 它在第二次调用RunAsync时不会占用更多内存。

但我们可以做得更好:

    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());
    }

在这个脚本中,我简化了我建议返回Action对象的解决方案,但我认为性能影响很小(但在实际实现中我真的认为你应该使用自己的接口来制作它灵活)。

当脚本运行时,你可以看到内存急剧上升到~240MB,但是在我调用垃圾收集器之后(为了演示目的,我在之前的代码上做了同样的事情)内存使用量下降了到~30MB。它也更快。

答案 1 :(得分:1)

我不确定这是否存在于问题创建时,但有一些非常相似的东西,让我们说official如何在不增加程序内存的情况下多次运行脚本。您需要使用 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 }));
}

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