使用C#结构的生产者/消费者?

Vla*_*lad 7 c# memory multithreading struct garbage-collection

我有一个处理请求的单例对象.每个请求大约需要一毫秒才能完成,通常更少.此对象不是线程安全的,它期望以特定格式的请求,封装在Request类中,并将结果返回为Response.该处理器具有另一个通过套接字发送/接收的生产者/消费者.

我实施了生产者/消费者方法来快速工作:

  • 客户端准备一个RequestCommand包含a TaskCompletionSource<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,所以它将被装箱.
  • 该命令也从线程跳转到线程.
  • Reciclying对象仅适用于命令本身,其内容无法回收(TaskCompletionSource<>和a string).

有什么方法可以利用结构来减少内存使用量和/或提高性能?还有其他选择吗?

Eri*_*ert 26

值类型可以在堆栈中实例化,但是如果它们从一个线程传递到线程,它们必须被复制到stackA-> heap和heap-> stackB我猜.

不,这根本不是真的.但是你的思想中存在更深层次的问题:

立即停止将结构视为生活在堆栈中.当你创建一个具有一百万个整数的int数组时,你认为这400万字节的整数存在于你的一百万字节堆栈中吗?当然不是.

事实上,堆栈与堆没有任何关系的值类型.而不是"堆栈和堆",开始说"短期分配池"和"长期分配池". 无论该变量是包含int还是对象的引用,都会从短期分配池中分配具有较短生存期的变量.一旦你开始正确地考虑变量生命周期,那么你的推理就变得非常简单了.短期内,短命的东西显然存在于短期内.

所以:当你将一个结构从一个线程传递到另一个线程时,它是否曾"存在于堆中"? 问题是荒谬的,因为价值观不是生活在堆上的东西. 变量是存储的东西; 变量存储值.

那么:是否将类转换为结构将提高性能,因为"那些结构可以存在于堆栈中"?不,当然不.引用类型和值类型之间的相关差异不是它们存在的位置,而是它们的复制方式.值类型按值复制,引用类型通过引用复制,引用副本是最快的副本.

我看到很多这些对象被创建并且顶部是内存中最常见的对象列表.请注意,没有内存泄漏,GC可以在每次触发时很好地清理所有内容,但显然很快就创建了很多对象,这使得Gen 0非常大.我想知道更好的内存使用是否会产生更好的性能.

好的,现在我们来看你问题的合理部分.这是一个很好的观察,它是一个可以用科学测试的.您应该做的第一件事是使用分析器来确定第0代集合对应用程序性能的实际负担.

可能这个负担不是你程序中最慢的东西,事实上它是无关紧要的.在这种情况下,您现在将知道将精力集中在真正的问题上,而不是追逐不是真正问题的内存分配问题.

假设您发现第0代收藏真的在扼杀您的表现; 你能做什么?是否有更多结构的答案?这可行,但你必须非常小心:

  • 如果结构本身包含引用,那么你只是将问题推到了一个级别,你还没有解决它.
  • 如果结构大于参考大小 - 当然它们几乎总是 - 那么现在你通过复制整个结构而不是复制引用来复制它们,并且你已经交换了GC时间问题以解决复制时间问题.这可能是胜利,也可能是亏损; 用科学来找出它是什么.

当我们在罗斯林面对这个问题时,我们非常仔细地考虑了这个问题并做了很多实验.我们采用的策略通常不是将事物移到堆栈上.相反,我们使用分析器确定每种类型的内存中有多少小的,短暂的活动在内存活动 - 然后在这些对象上实现池化策略.你需要一个小物件,你把它带出池子.当你完成后,你把它放回池中.结果是,你最终得到了池中的O(任何时候都有活动的对象数),它很快被移动到第2代堆中; 然后,你会大大降低第0代堆上的收集压力,同时增加相对罕见的第2代收藏品的成本.

我不是说这是你的最佳选择.我说我们在罗斯林遇到了同样的问题,我们用科学解决了这个问题.你也可以做到的.

  • @LucaCremonesi:当然它们是个体变量.它们包含值,**它们可以变化**.我们称变量变量,因为变量是*能够变化的东西*.你可以在任何需要变量*的上下文中使用`a [0]` - 你可以把它放在赋值的左边,你可以把它作为`ref`参数,依此类推.这是一个变量! (2认同)