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

3
我正在编写一个多线程解决方案,用于将来自不同源的数据传输到中央数据库。该解决方案通常包括两个部分:
  1. 单线程导入引擎
  2. 调用导入引擎的多线程客户端。
为了最小化定制开发,我使用了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作为全局参数传递到脚本引擎中,而不是为每个字段提供脚本文本。在脚本的某个位置,我们需要切换到执行并返回适当值的特定代码部分。
这种解决方案的好处应该是更好的内存使用率。缺点是脚本维护从其使用的特定点中移除了。
在实施此更改之前,我想听听关于此解决方案的意见和不同方法的建议。
2个回答

4
似乎使用脚本完成任务可能是一种过度浪费的做法——您会使用许多应用程序层,导致内存占用过高。
其他解决方案:
- 您如何与数据库进行接口?您可以根据需要操纵查询本身,而不是为此编写整个脚本。 - 如何使用泛型?通过足够的T来满足您的需求: ``` public class ImportEngine ``` - 使用元组(这与使用泛型非常相似)
但是,如果您仍然认为脚本是适合您的正确工具,我发现可以通过在应用程序内部运行脚本工作(而不是使用RunAsync)来降低脚本的内存使用率。您可以通过从RunAsync获取逻辑并重复使用它来实现这一点,而不是在内存占用较高的RunAsync内部执行工作。以下是一个示例:
而不是简单地(脚本字符串):
DoSomeWork();

你可以这样做(IHaveWork是你的应用程序中定义的一个接口,只有一个方法Work):
public class ScriptWork : IHaveWork
{
    Work()
    {
        DoSomeWork();
    }
}
return new ScriptWork();

这种方式只在短时间内调用了重型的RunAsync,并返回一个可在应用程序中重复使用的工作者(当然,您也可以通过向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。它也更快。

导入引擎是更大工具集的一小部分。而且,使用脚本编写对于整个解决方案都有很大帮助。因此,目前使用 Roslyn 实现的这种类型的脚本非常完美。就内存消耗而言,我注意到在 ScriptObject.Compile(); 之后,内存使用量增加了近 60 MB。所以,我为每个定义的脚本调用 _Compile_,这很快就会浪费所有内存。如果使用相同,我不确定您的建议如何有所帮助。请注意,在调用 RunAsync 时,内存已经被消耗。 - Vladimir.RL
获取 IHaveWork 对象后,您可以处理脚本并考虑仅使用 RunAsync 来代替。这将为您完成处理。 - idanp
好的。我已经更改了测试代码以返回新对象。我仍然不明白如何处理脚本。除非您考虑为脚本执行添加额外的AppDomain? - Vladimir.RL
@Vladimir.RL,请看我的编辑。如果您不引用脚本或直接使用RunAsync,垃圾收集器会为您完成它。 - idanp
很高兴听到它有所帮助。我没有一个“地方”来写关于它的内容,我在想这种技巧是否值得在这里的文档中提及... - idanp
显示剩余2条评论

2
我不确定在问题创建时是否存在这种情况,但有一些非常相似的东西,让我们说,官方的方法可以多次运行脚本而不增加程序内存。您需要使用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保存在全局列表中并稍后快速调用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接