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

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

  1. 单线程导入引擎
  2. 在线程中调用Import引擎的multithreading客户端。

为了最大限度地减少自定义开发,我使用的是Roslyn脚本。 导入引擎项目中的Nuget包管理器启用了此function。 每个导入都被定义为输入表的转换 – 具有输入字段的集合 – 再次转换为目标表 – 再次使用目标字段的集合。

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

//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(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 RunScript() { return (await ScriptObject.RunAsync(_ScriptHost)).ReturnValue.ToString(); } 

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

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

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

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

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

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

其他方案:

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

    public class ImportEngine

  • 使用元组 (这非常类似于使用generics)

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

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

 DoSomeWork(); 

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

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

这样你只能在短时间内调用繁重的RunAsync,它会返回一个你可以在你的应用程序中重复使用的worker(你当然可以通过向Work方法添加参数来扩展它,并从你的应用程序inheritance逻辑,所以上…)。

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

编辑

一些快速基准:

这段代码:

  static void Main(string[] args) { Console.WriteLine("Compiling"); string code = "System.Threading.Thread.SpinWait(100000000); System.Console.WriteLine(\" Script end\");"; List> 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()); } 

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

但我们可以做得更好:

  static void Main(string[] args) { Console.WriteLine("Compiling"); string code = "return () => { System.Threading.Thread.SpinWait(100000000); System.Console.WriteLine(\" Script end\"); };"; List scripts = Enumerable.Range(0, 50).Select(async num => await CSharpScript.EvaluateAsync(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 。 它也更快。

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

我会在这里发布它只是为了方便:

 var script = CSharpScript.Create("X*Y", globalsType: typeof(Globals)); ScriptRunner runner = script.CreateDelegate(); for (int i = 0; i < 10; i++) { Console.WriteLine(await runner(new Globals { X = i, Y = i })); } 

它最初需要一些内存,但是将运行器保留在某个全局列表中并稍后快速调用它。