【问题标题】:gRPC client do not dispose ChannelgRPC 客户端不配置 Channel
【发布时间】:2018-05-15 14:54:17
【问题描述】:

我正在使用 gRPC 开发 .net core 2.0 应用程序并发现一个问题:删除对我的 gRPC 客户端类实例的引用后,仍然存在使用资源(内存和处理器)的通道。 示例代码:

public class MyClient : ClientBase
    {
        public MyClient(Channel channel) : base(channel)
        {
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var list = new List<MyClient>();
            for (var i = 0; i < 10000; i++)
            {
                Console.WriteLine($"Creating {i} instance");
                list.Add(new MyClient(new Channel("127.0.0.1:61783", ChannelCredentials.Insecure)));
            }

            Console.WriteLine("press enter to list = null");
            Console.ReadLine();
            list = null;

            Console.WriteLine("press enter to GC.Collect();");
            Console.ReadLine();
            GC.Collect();

            Console.WriteLine("press enter to exit");
            Console.ReadLine();
        }
    }

如果你运行示例,你会看到这个应用程序使用了 10%(在我的 PC 上)。即使在 list = nullGC.Collect()

之后

我认为原因是 ClientBase 不要调用 Channel.ShutdownAsync()

所以问题是:

解决问题的更好方法是什么?

附言实际上我使用的是“由协议缓冲区编译器生成的”客户端

Client: Grpc.Core.ClientBase<TDto>

我无法在生成的类中显式更改终结器

【问题讨论】:

  • 你为什么不自己关闭频道?
  • 我不确定这是不是最好的解决方案:我认为使用 using{} 或在 finilize() 方法处理资源的最佳做法...
  • 您正在使用不打算像这样使用的第三方库(没有实现IDisposable,甚至是通道),因此在这种情况下,最好的方法是在您使用时显式关闭通道与它们一起完成,而不是依赖任何东西(如垃圾收集器)为你做这件事。好吧,最好不要依赖 GC 来释放一些你无论如何都可以自己释放的资源。
  • @TimurLemeshko 让您的客户端实现IDisposable 并在Dispose 方法中调用base.Channel.ShutdownAsync()。这将允许您现在在客户端上调用 Dispose 并释放资源或将它们包装在 using 中,以便在它们超出范围时为您完成
  • @Nkosi 那里没有base.Channel。但即使有 - ShutdownAsync() 返回 Task,所以只在 dispose 中执行 ShutdownAsync() 似乎是不对的(并且执行 ShutdownAsync().Wait() 似乎也不正确)。

标签: c# memory-leaks asp.net-core grpc


【解决方案1】:

可能的建议是让客户端实现IDisposable 并在Dispose 方法中调用Channel.ShutdownAsync()

public class MyClass : Client, IDisposable {
    Channel channel;
    private bool _isDisposed = false;
    private readonly object _lock = new object();

    public MyClass(Channel channel)
        : base(channel) {
        this.channel = channel;
        this.channelDisposing += onDisposing;
    }

    public Channel Channel { get { return channel; } }

    private event EventHandler channelDisposing = delegate { };

    async void onDisposing(object sender, EventArgs e) {
        await channel.ShutdownAsync();
        channel = null;
    }

    public void Dispose() {
        if (!_isDisposed) {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    void Dispose(bool disposing) {
        // No exception should ever be thrown except in critical scenarios.
        // Unhandled exceptions during finalization will tear down the process.
        if (!_isDisposed) {
            try {
                if (disposing) {
                    // Acquire a lock on the object while disposing.
                    if (channel != null) {
                        lock (_lock) {
                            if (channel != null) {
                                channelDisposing(this, EventArgs.Empty);
                            }
                        }
                    }
                }
            } finally {
                // Ensure that the flag is set
                _isDisposed = true;
            }
        }
    }
}

这将允许您现在在客户端上调用 Dispose 并释放资源或将它们包装在 using 中,以便在它们超出范围时为您完成。

public class Program {
    public static void Main(string[] args) {
        var list = new List<MyClient>();
        for (var i = 0; i < 10000; i++) {
            Console.WriteLine($"Creating {i} instance");
            list.Add(new MyClient(new Channel("127.0.0.1:61783", ChannelCredentials.Insecure)));
        }

        Console.WriteLine("press enter to dispose clients");
        Console.ReadLine();
        list.ForEach(c => c.Dispose());

        Console.WriteLine("press enter to list = null");
        Console.ReadLine();
        list = null;

        Console.WriteLine("press enter to GC.Collect();");
        Console.ReadLine();
        GC.Collect();

        Console.WriteLine("press enter to exit");
        Console.ReadLine();
    }
}

【讨论】:

  • 我不能(是吗?!)更改 MyClient 类,因为它是由 Grpc 自动生成的(请参阅我在帖子中的 ps)。只有你知道如何使用 Grpc 生成器来改变它......
  • @TimurLemeshko 然后创建另一个继承自MyClass 并实现IDisposable 的类,前提是生成的类没有密封。我的假设是您可以控制MyClass 代码。
  • 我不太喜欢这种方式。在 dispose 中返回 Task 的调用方法(即使使用 GetAwaiter().GetResult())看起来不正确。最好允许调用者直接访问ShutdownAsync,然后它可以为他们做类似Task.WhenAll() 的事情,并且通常将其集成到已经异步的代码库中。
  • @Evk 是的,我倾向于同意。这就是为什么我公开了Channel 属性以使该路由可用。我知道它不漂亮。这只是一个建议,直到出现更好的建议。 3rd 方依赖项看起来设计不佳,这迫使这种不受欢迎的解决方法。
猜你喜欢
  • 2018-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-22
  • 1970-01-01
相关资源
最近更新 更多