【问题标题】:Producer-Consumer multiple producers multiple queues single consumer [closed]生产者-消费者多个生产者多个队列单个消费者[关闭]
【发布时间】:2016-07-10 22:11:03
【问题描述】:

我们有一个一般的日志记录问题,我们执行 3 种类型的日志记录(让我们说:跟踪、审计、计数)是我们执行的 3 个日志记录领域。我们同时执行所有这些操作,从在 REST Web 服务中运行的代码开始,针对传入的每个 Web 请求。

对于每个发出的请求,我们都会对每个日志记录区域进行多次调用。可以说,每个 Web 请求、一些跟踪、一些审计、一些计数器、每个 Web 请求总共有 100 个调用,平均总共有大约 100 个日志记录调用。每个日志调用都会将一些数据写入某个存储以供以后处理[存储是什么并不重要,但是将其放入该存储肯定是 I/O 绑定的——(实际上它是一个 azure 队列,因此它是跨互联网的 HTTP 调用) ]。

我们的问题是,记录任何信息(在 3 个区域中的任何一个中)的行为会使服务请求线程花费太长的时间来写入所有日志接收器 100 次。我们测量到多达 4/5 的请求处理时间用于写入日志接收器!因此,您可以看到我们需要对其进行显着优化!

我们想要加快速度,并让请求线程免于写入日志。我们想使用另一个线程在后台按照自己的节奏将日志数据写入 3 个日志存储。因此,我们认为将日志写入排队到内存队列中,以便日志写入线程可以在多个请求进入时并行处理它们,这是一种可行的方法。

Stephen Cleary 的“Concurrency in C# Cookbook”一书(fab 参考)指出,在这种情况下,使用像 BlockingCollection<T> 这样的阻塞集合是理想的。这样一个线程一次可以产生数据(并写入内存队列),另一个线程可以消耗内存队列中的数据。看起来这对我们的情况来说是理想的。

但是,在我们的例子中,因为我们在 ASP.NET 主机中运行,并且线程池线程对于 Web 服务器来说非常宝贵(为了可扩展性),所以我们只希望有一个线程专用于 [消费] 所有3 个日志记录队列。让所有其他线程处理入站 Web 请求,并 [产生] 日志数据。

那么问题就变成了:如何使用BlockingCollection<T>的3个实例(每种日志类型一个),并支持:

  1. 用于生产数据的任意数量的生产者 [入站 Web 请求线程]
  2. 单个(专用)日志记录线程使用者,始终连续高效地消耗所有三个队列。

任何人都可以想到一个在这里可以很好地工作的设计模式吗?对我们来说,缺少的部分是消费者如何有效地清空所有三个队列,而不阻塞其中任何一个,并持续处理所有三个队列。

【问题讨论】:

  • 您可以有一个BlockingCollection<T>,其中T 是一些自定义类,可以包含三种类型中的任何一种。您可以在此类型中使用枚举来检测该类真正拥有的类型。
  • 谢谢,但我没有从您的回答中看到我们如何使用相同的消费者线程连续处理所有三个 BlockingCollection<T> 队列,而无需等待任何一个队列。
  • 我建议你有一个BlockingCollection,而不是三个。
  • 我现在明白了。感谢您的澄清。枚举描述了数据,最终帮助消费者决定将其添加到哪个商店。是的,这可以工作。

标签: c# multithreading asynchronous concurrency producer-consumer


【解决方案1】:

这是我在类似场景中成功使用的模式。

首先,我通常使用ConcurrentQueue<T>,因为没有需要阻止任何东西的场景。也许这会根据您的日志记录量而有所不同。该队列包含在Buffer 类中。它可以包含多个队列。

请求线程所做的只是将项目放入队列中。在同一个线程上,Buffer 正在根据最大大小检查数量,如果它确定需要刷新缓冲区,它会在单独的线程上执行此操作。还有一个计时器,即使缓冲区未满,缓冲区也会被刷新。在任何一种情况下,都会检查以确保缓冲区尚未刷新。

结果是,除非缓冲区永远满了,否则不需要一个线程始终专用于日志,在没有消息时阻塞。

您还可以通过将消息批量发送到队列而不是单独发送来提高性能。您可以通过发送包含 50-100 条消息的“消息”而不是单独发送每条消息来减少大量开销。

【讨论】:

  • 感谢@Scott,这里有一些好主意,以及您建议的一些优化在我们的场景中是有意义的。我特别喜欢添加超时和最大阈值。您是否建议我们使用 Task.Run() 启动日志记录线程(来自请求线程)?
  • 是的。我有一个通用的实现,但我找不到它,所以我再写一次。这个想法是一个通用的ISink<T> 接口,BufferedSink<T> 是一个实现。这反过来又包含它写入的另一个ISink<T>。它还依赖于IBufferedSinkSettings 接口,该接口包含最大缓冲区大小、计时器间隔,还可以指定刷新到内部缓冲区是否应该是同步的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-17
  • 2018-03-07
  • 2011-11-08
  • 2016-09-11
  • 2020-06-17
  • 1970-01-01
相关资源
最近更新 更多