如何在多个脚本的批处理中使用 Roslyn C# 脚本?
How to use Roslyn C# scripting in batch processing with several scripts?
我正在编写多线程解决方案,用于将数据从不同来源传输到中央数据库。解决方案,一般来说,有两部分:
- 单线程导入引擎
- 在线程中调用导入引擎的多线程客户端。
为了最大程度地减少自定义开发,我使用了 Roslyn 脚本。此功能通过导入引擎项目中的 Nuget 包管理器启用。
每个导入都定义为输入 table 的转换——具有输入字段的集合——到目标 table——再次与目标字段的集合。
此处使用脚本引擎允许输入和输出之间的自定义转换。对于每个 input/output 对,都有一个带有自定义脚本的文本字段。这是用于脚本初始化的简化代码:
//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();
}
因此,在导入定义初始化之后,我们可能有任意数量的 input/output 对描述和脚本对象,内存占用量增加大约 50 MB 每对定义脚本。
类似的使用模式适用于在将目标行存储到数据库之前对其进行验证(每个字段可能有多个用于检查数据有效性的脚本)。
总而言之,适度 transformation/validation 脚本的典型内存占用是每个线程 200 MB。如果我们需要调用多个线程,内存使用率将非常高,99% 将用于脚本。
如果 Import 引擎包含在基于 WCF 的中间层(我这样做)中,我们很快就会遇到 "Insufficient memory" 问题。
显而易见的解决方案是拥有一个脚本实例,该实例会根据需要(input/output 转换、验证或其他)以某种方式将代码执行分派给脚本内的特定函数。 IE。我们将有 SCRIPT_ID 而不是每个字段的脚本文本,它将作为全局参数传递给脚本引擎。在脚本的某个地方,我们需要切换到将要执行的代码的特定部分和 return 适当的值。
这种解决方案的好处应该是显着提高内存使用率。缺点是脚本维护已从使用它的特定点移除。
在实施此更改之前,我想听听有关此解决方案的意见以及对不同方法的建议。
看起来 - 为任务使用脚本可能是一种浪费的矫枉过正 - 你使用了很多应用程序层并且内存变满了。
其他解决方案:
- 您如何与数据库交互?您可以根据自己的需要操作查询本身,而不是为此编写整个脚本。
使用泛型怎么样?有足够的 T 来满足您的需求:
public class ImportEngine<T1,T2,T3,T3,T5>
使用Tuples(这很像使用泛型)
但是,如果您仍然认为脚本是适合您的工具,我发现脚本的内存使用量可以通过 运行 在您的应用程序中运行的脚本来降低(而不是使用 RunAsync),您可以通过从 RunAsync 取回逻辑并重新使用它来做到这一点,而不是在繁重且浪费内存的工作中进行 RunAsync
。这是一个例子:
而不是简单地(脚本字符串):
DoSomeWork();
你可以这样做(IHaveWork是你app中定义的一个接口,只有一个方法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。它也更快。
我不确定这在创建问题时是否存在,但有一些非常相似的东西,比方说,official 如何在不增加程序内存的情况下多次 运行 脚本。您需要使用 CreateDelegate 方法来完成预期的工作。
为了方便起见,我会post这里:
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 }));
}
它最初需要一些内存,但将 运行ner 保存在一些全局列表中,稍后快速调用它。
我正在编写多线程解决方案,用于将数据从不同来源传输到中央数据库。解决方案,一般来说,有两部分:
- 单线程导入引擎
- 在线程中调用导入引擎的多线程客户端。
为了最大程度地减少自定义开发,我使用了 Roslyn 脚本。此功能通过导入引擎项目中的 Nuget 包管理器启用。 每个导入都定义为输入 table 的转换——具有输入字段的集合——到目标 table——再次与目标字段的集合。
此处使用脚本引擎允许输入和输出之间的自定义转换。对于每个 input/output 对,都有一个带有自定义脚本的文本字段。这是用于脚本初始化的简化代码:
//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();
}
因此,在导入定义初始化之后,我们可能有任意数量的 input/output 对描述和脚本对象,内存占用量增加大约 50 MB 每对定义脚本。 类似的使用模式适用于在将目标行存储到数据库之前对其进行验证(每个字段可能有多个用于检查数据有效性的脚本)。
总而言之,适度 transformation/validation 脚本的典型内存占用是每个线程 200 MB。如果我们需要调用多个线程,内存使用率将非常高,99% 将用于脚本。 如果 Import 引擎包含在基于 WCF 的中间层(我这样做)中,我们很快就会遇到 "Insufficient memory" 问题。
显而易见的解决方案是拥有一个脚本实例,该实例会根据需要(input/output 转换、验证或其他)以某种方式将代码执行分派给脚本内的特定函数。 IE。我们将有 SCRIPT_ID 而不是每个字段的脚本文本,它将作为全局参数传递给脚本引擎。在脚本的某个地方,我们需要切换到将要执行的代码的特定部分和 return 适当的值。
这种解决方案的好处应该是显着提高内存使用率。缺点是脚本维护已从使用它的特定点移除。
在实施此更改之前,我想听听有关此解决方案的意见以及对不同方法的建议。
看起来 - 为任务使用脚本可能是一种浪费的矫枉过正 - 你使用了很多应用程序层并且内存变满了。
其他解决方案:
- 您如何与数据库交互?您可以根据自己的需要操作查询本身,而不是为此编写整个脚本。
使用泛型怎么样?有足够的 T 来满足您的需求:
public class ImportEngine<T1,T2,T3,T3,T5>
使用Tuples(这很像使用泛型)
但是,如果您仍然认为脚本是适合您的工具,我发现脚本的内存使用量可以通过 运行 在您的应用程序中运行的脚本来降低(而不是使用 RunAsync),您可以通过从 RunAsync 取回逻辑并重新使用它来做到这一点,而不是在繁重且浪费内存的工作中进行 RunAsync
。这是一个例子:
而不是简单地(脚本字符串):
DoSomeWork();
你可以这样做(IHaveWork是你app中定义的一个接口,只有一个方法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。它也更快。
我不确定这在创建问题时是否存在,但有一些非常相似的东西,比方说,official 如何在不增加程序内存的情况下多次 运行 脚本。您需要使用 CreateDelegate 方法来完成预期的工作。
为了方便起见,我会post这里:
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 }));
}
它最初需要一些内存,但将 运行ner 保存在一些全局列表中,稍后快速调用它。