【问题标题】:Ripple Effect: OutOfMemoryException涟漪效应:OutOfMemoryException
【发布时间】:2016-04-25 16:44:13
【问题描述】:

我一直在尝试了解 Roslyn,看看它是否适合我的需求。

在一个非常简单的项目中,我试图创建一个简单的“涟漪效应”,每次迭代都会导致加载一个新程序集,并最终在 500 次迭代后崩溃(OutOfMemoryException)

有没有办法做到这一点而不会导致爆炸?

class Program
{
    static void Main(string[] args)
    {
        string code = @"
        IEnumerable<double> combined = A.Concat(B);
        return combined.Average();                    
        ";

        Globals<double> globals = new Globals<double>()
        {
            A = new double[] { 1, 2, 3, 4, 5 },
            B = new double[] { 1, 2, 3, 4, 5 },
        };

        ScriptOptions options = ScriptOptions.Default;            
        Assembly systemCore = typeof(Enumerable).Assembly;
        options = options.AddReferences(systemCore);
        options = options.AddImports("System");
        options = options.AddImports("System.Collections.Generic");
        options = options.AddImports("System.Linq");

        var ra = CSharpScript.RunAsync(code, options, globals).Result;

        for (int i = 0; i < 1000; i++)
        {
            ra = ra.ContinueWithAsync(code).Result;
        }            
    }
}

public class Globals<T>
{
    public IEnumerable<T> A;
    public IEnumerable<T> B;
}

Exception Image

【问题讨论】:

    标签: c# scripting roslyn .net-4.6


    【解决方案1】:

    每次您使用 CSharpScript.Run 或 Evaluate 方法时,您实际上是在加载一个恰好相当大的新脚本(一个 .dll)。为了避免这种情况,您需要缓存您正在执行的脚本:

    _script = CSharpScript.Create<TR>(code, opts, typeof(Globals<T>)); // Other options may be needed here
    

    缓存了 _script 您现在可以通过以下方式执行它:

    _script.RunAsync(new Globals<T> {A = a, B = b}); // The script will compile here in the first execution
    

    如果您每次都需要为应用程序加载一些脚本,那么这是最简单的做法。然而,更好的解决方案是使用单独的 AppDomain 并加载隔离的脚本。这是一种方法:

    创建一个作为 MarshalByRefObject 的脚本执行器代理:

    public class ScriptExecutor<TP, TR> : CrossAppDomainObject, IScriptExecutor<TP, TR>
    {
        private readonly Script<TR> _script;
        private int _currentClients;
    
        public DateTime TimeStamp { get; }
        public int CurrentClients => _currentClients;
        public string Script => _script.Code;
    
        public ScriptExecutor(string script, DateTime? timestamp = null, bool eagerCompile = false)
        {
            if (string.IsNullOrWhiteSpace(script))
                throw new ArgumentNullException(nameof(script));
    
            var opts = ScriptOptions.Default.AddImports("System");
            _script = CSharpScript.Create<TR>(script, opts, typeof(Host<TP>)); // Other options may be needed here
            if (eagerCompile)
            {
                var diags = _script.Compile();
                Diagnostic firstError;
                if ((firstError = diags.FirstOrDefault(d => d.Severity == DiagnosticSeverity.Error)) != null)
                {
                    throw new ArgumentException($"Provided script can't compile: {firstError.GetMessage()}");
                }
            }
            if (timestamp == null)
                timestamp = DateTime.UtcNow;
            TimeStamp = timestamp.Value;
        }
    
        public void Execute(TP parameters, RemoteCompletionSource<TR> completionSource)
        {
            Interlocked.Increment(ref _currentClients);
           _script.RunAsync(new Host<TP> {Args = parameters}).ContinueWith(t =>
           {
               if (t.IsFaulted && t.Exception != null)
               {
                   completionSource.SetException(t.Exception.InnerExceptions.ToArray());
                   Interlocked.Decrement(ref _currentClients);
               }
               else if (t.IsCanceled)
               {
                   completionSource.SetCanceled();
                   Interlocked.Decrement(ref _currentClients);
               }
               else
               {
                   completionSource.SetResult(t.Result.ReturnValue);
                   Interlocked.Decrement(ref _currentClients);
               }
           });
        }
    }
    
    public class Host<T>
    {
        public T Args { get; set; }
    }
    

    创建一个代理对象,在脚本执行应用域和主域之间共享数据:

    public class RemoteCompletionSource<T> : CrossAppDomainObject
    {
        private readonly TaskCompletionSource<T> _tcs = new TaskCompletionSource<T>();
    
        public void SetResult(T result) { _tcs.SetResult(result); }
        public void SetException(Exception[] exception) { _tcs.SetException(exception); }
        public void SetCanceled() { _tcs.SetCanceled(); }
    
        public Task<T> Task => _tcs.Task;
    }
    

    创建所有其他远程需要继承的辅助抽象类型:

    public abstract class CrossAppDomainObject : MarshalByRefObject, IDisposable
    {
    
        private bool _disposed;
    
        /// <summary>
        /// Gets an enumeration of nested <see cref="MarshalByRefObject"/> objects.
        /// </summary>
        protected virtual IEnumerable<MarshalByRefObject> NestedMarshalByRefObjects
        {
            get { yield break; }
        }
    
        ~CrossAppDomainObject()
        {
            Dispose(false);
        }
    
        /// <summary>
        /// Disconnects the remoting channel(s) of this object and all nested objects.
        /// </summary>
        private void Disconnect()
        {
            RemotingServices.Disconnect(this);
    
            foreach (var tmp in NestedMarshalByRefObjects)
                RemotingServices.Disconnect(tmp);
        }
    
        public sealed override object InitializeLifetimeService()
        {
            //
            // Returning null designates an infinite non-expiring lease.
            // We must therefore ensure that RemotingServices.Disconnect() is called when
            // it's no longer needed otherwise there will be a memory leak.
            //
            return null;
        }
    
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            Dispose(true);
        }
    
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
                return;
    
            Disconnect();
            _disposed = true;
        }
    }
    

    我们是这样使用它的:

    public static IScriptExecutor<T, R> CreateExecutor<T, R>(AppDomain appDomain, string script)
        {
            var t = typeof(ScriptExecutor<T, R>);
            var executor = (ScriptExecutor<T, R>)appDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName, false, BindingFlags.CreateInstance, null, 
                new object[] {script, null, true}, CultureInfo.CurrentCulture, null);
            return executor;
        }
    
    public static AppDomain CreateSandbox()
    {
        var setup = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase };
        var appDomain = AppDomain.CreateDomain("Sandbox", null, setup, AppDomain.CurrentDomain.PermissionSet);
        return appDomain;
    }
    
    string script = @"int Square(int number) {
                          return number*number;
                      }
                      Square(Args)";
    
    var domain = CreateSandbox();
    var executor = CreateExecutor<int, int>(domain, script);
    
    using (var src = new RemoteCompletionSource<int>())
    {
        executor.Execute(5, src);
        Console.WriteLine($"{src.Task.Result}");
    }
    

    注意在 using 块中 RemoteCompletionSource 的使用。如果您忘记处置它,您将有内存泄漏,因为此对象在另一个域(不是调用者)上的实例将永远不会被 GC。

    免责声明:我从 RemoteCompletionSource 的想法 here,也是来自公共领域的 CrossAppDomainObject 的想法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-07
      • 2017-03-10
      • 2018-02-23
      • 2021-07-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多