c# - 具有 C# 结构的生产者/消费者?
问题描述
我有一个处理请求的单例对象。每个请求大约需要一毫秒才能完成,通常更少。该对象不是线程安全的,它需要特定格式的请求,封装在Request
类中,并将结果返回为Response
. 该处理器有另一个生产者/消费者,通过套接字发送/接收。
我实施了生产者/消费者方法来快速工作:
- 客户端准备一个
RequestCommand
命令对象,其中包含 aTaskCompletionSource<Response>
和预期的Request
. - 客户端将命令添加到“请求队列”(
Queue<>
)并等待command.Completion.Task
。 - 一个不同的线程(和实际的后台
Thread
)从“请求队列”中拉出命令,处理command.Request
,生成Response
命令并用command.Completion.SetResult(response)
. - 客户继续工作。
但是当做一个小的内存基准测试时,我看到很多这些对象被创建并且在内存中最常见的对象列表中名列前茅。请注意,没有内存泄漏,GC 可以在每次触发时很好地清理所有内容,但显然如此多的对象被快速创建,使得 Gen 0 非常大。我想知道更好的内存使用是否会产生更好的性能。
我正在考虑将其中一些对象转换为结构以避免分配,特别是现在有一些新功能可以使用它们 C# 7.1。但我没有看到这样做的方法。
- 值类型可以在堆栈中实例化,但如果它们从线程传递到线程,则必须将它们复制到 stackA->heap 和 heap->stackB 我猜。同样,当在队列中排队时,它会从堆栈到堆。
- 单例对象是真正异步的。有一些内存处理,但 90% 的时间它需要调用外部并通过内部生产者/消费者。
ValueTask<>
似乎不适合这里,因为事情是异步的。TaskCompletionSource<>
有一个状态,但它是object
,所以它会被装箱。- 该命令还从一个线程跳转到另一个线程。
- 回收对象仅适用于命令本身,其内容不能回收(
TaskCompletionSource<>
和 astring
)。
有什么方法可以利用结构来减少内存使用或/和提高性能?还有其他选择吗?
解决方案
值类型可以在堆栈中实例化,但如果它们从线程传递到线程,则必须将它们复制到 stackA->heap 和 heap->stackB 我猜。
不,这根本不是真的。但是您在这里的想法中有一个更深层次的问题:
立即停止将结构视为存在于堆栈中的想法。当你用一百万个整数创建一个整数数组时,你认为这四百万字节的整数存在于你的一百万字节堆栈中吗?当然不是。
事实是堆栈与堆与值类型没有任何关系。开始说“短期分配池”和“长期分配池”,而不是“堆栈和堆”。 具有短生命周期的变量是从短期分配池中分配的,无论该变量是否包含 int 或对对象的引用。一旦你开始正确地考虑变量生命周期,那么你的推理就变得非常简单了。很明显,短命的东西存在于短期池中。
所以:当你将一个结构体从一个线程传递到另一个线程时,它是否存在于“堆上”? 这个问题是荒谬的,因为值不是存在于堆上的东西。 变量是存储的东西;变量存储值。
那么:将类转换为结构是否会提高性能,因为“那些结构可以存在于堆栈上”?不,当然不。引用类型和值类型之间的相关区别不在于它们所在的位置,而在于它们的复制方式。值类型按值复制,引用类型按引用复制,引用复制是最快的复制。
我看到很多这些对象被创建并在内存中最常见的对象列表中名列前茅。请注意,没有内存泄漏,GC 可以在每次触发时很好地清理所有内容,但显然如此多的对象被快速创建,使得 Gen 0 非常大。我想知道更好的内存使用是否会产生更好的性能。
好的,现在我们来到你问题的明智部分。这是一个极好的观察结果,它是可以用科学检验的。您应该做的第一件事是使用分析器来确定第 0 代集合对应用程序性能的实际负担。
可能这个负担不是你程序中最慢的事情,实际上它是无关紧要的。在这种情况下,您现在将知道将精力集中在真正的问题上,而不是追查不是真正问题的内存分配问题。
假设您发现 gen 0 集合确实在扼杀您的表现;你能做什么?是让更多的东西结构的答案吗?这可以工作,但你必须非常小心:
- 如果结构本身包含引用,那么您只是将问题推到了一层,您还没有解决它。
- 如果结构大于引用大小——当然它们几乎总是如此——那么现在你通过复制整个结构而不是复制引用来复制它们,并且你已经将 GC 时间问题换成了复制时间问题. 这可能是胜利,也可能是失败;用科学找出它是什么。
当我们在罗斯林遇到这个问题时,我们考虑得很仔细,做了很多实验。我们采用的策略通常是不将东西移到堆栈上。相反,我们使用分析器确定了在任何时候内存中有多少小、短寿命的对象处于活动状态,每种类型- 使用分析器 - 然后对这些对象实施池化策略。你需要一个小物件,你把它从水池里拿出来。完成后,将其放回池中。发生的情况是,您最终会得到O(任何时候活动的对象数)在池中,它很快被移入第 2 代堆;然后,您会大大降低对第 0 代堆的收集压力,同时增加相对稀有的第 2 代收集的成本。
我并不是说这对你来说是最好的选择。我是说我们在罗斯林也遇到了同样的问题,我们用科学解决了这个问题。你也可以做到的。
推荐阅读
- node.js - gcloud.app.deploy 错误响应:[9] 应用程序启动错误:public@1.0.0 start /app node app.js
- java - 使用 IntelliJ 将 Java lambda 转换为 Kotlin Lamba 生成语法错误
- html - 水平和垂直滚动时的固定列
- c++ - new 和 delete 重载 - 为错误的内存调用重载删除
- c# - 关系映射实体框架
- python - opencv鼠标回调没有被触发
- ios - 将 Swift 委托分配给 Objective-C 对象时出错
- excel - 如何打开现有的PowerPoint并粘贴Excel范围
- amazon-web-services - AWS 中的规范用户 ID 有什么用?
- terraform - AWS ECS 优化的容器日志通过 Terraform 移动到 cloudwatch 日志