内存队列






4.93/5 (12投票s)
内存队列、线程队列和基本日志框架的初步实现
引言
这篇文章是我一直想写很久了,很高兴我选择它作为我在Code Project上发布的第一个文章。所以,一方面,我很高兴能介绍一些我一直在使用并且可能会继续在我的一些个人项目上使用的东西;另一方面,我有点紧张地将这些东西发布到网上,让别人看到,并希望他们能以积极批判的态度进行评论。这篇文章是关于我每当我需要在多线程环境中需要内存队列,或者我想为专门的线程处理器分配一些工作时,所使用的非常小巧简单的对象。我将通过使用N-Unit框架编写的非常简单直接的单元测试来展示它是如何工作的,然后我将使用它来实现一个处理器系统以及一个以惊人的速度将日志写入文本文件的日志记录器,而不会影响我的主处理线程。希望您也能看到一些可以简化您自己工作的场景。
目录
- 背景
- 场景 1 – 线程队列
- 场景 2 – 日志记录器
- 使用代码
- 有用链接
- 结论
我希望以上内容足以让您对如何使用内存队列有一个很好的了解。
背景
我一直需要一种方法来将工作分配给各种线程,而不必过多地担心同步问题,并且可以将实际工作的负载从我的主线程中移除。当然,你们中的一些人可能会认为我可以使用异步委托来做我将要展示的内容,或者我可以使用响应式扩展,虽然在某些情况下我可能会同意你们的观点,但在其他情况下我也会不同意。我喜欢尽可能地保持简单,当你构建一个框架或应用程序时,你会很快意识到通过使用一些定义明确的模式或一些第三方工具集和框架,事情可以变得更简单,但同时,我一直是一个喜欢自己动手编码的人,因为通常是我需要支持这个产品一段时间。虽然第三方工具集在我们今天做的很多事情中都非常有帮助,但它也会在我们的做事方式上引入限制,而且这还不包括学习周期或过程的开销。如果有一种更简单、更快捷的方式以一致的方式编写代码,那为什么不呢?那么,让我们开始讨论场景吧……
场景 1 – 线程队列
在这个场景中,我将使用内存队列来编写一个简单高效的线程队列,它将允许您实现命令模式,从而可以轻松地分离您想要添加的业务价值,而不会影响主进程。
场景 2 – 日志记录器
在这个场景中,我将实现一个小的日志框架,它使用上述的线程队列,其中我将实现的命令是将数据写入一个分隔符文件中。这将允许您为日志记录器提供一个特定的格式化程序,在将其写入分隔符文件格式之前,该格式化程序将按照您想要的方式格式化数据。当日志文件达到特定行数时,此日志记录器还会压缩文件,从而使我们的日志大小保持可管理。我需要注意的一点是,因为实际的文件处理和写入是在与我们的业务流程不同的线程上完成的,所以它不像我们在内联日志记录时那样干扰。另一件事是,这个日志记录器非常轻量级和快速,同时允许您添加更多的命令类型,并且配置起来非常容易。
Using the Code
解决方案结构
解决方案资源管理器
概述
以上解决方案的简要概述是,您将看到有三个项目包含我们感兴趣的代码,以及两个用于测试代码的测试项目。我将在接下来的部分分解项目,解释每个项目的作用。三个实现项目分别为:Yakiloo.Interfaces
、Yakiloo.Queuing
、Yakiloo.Logging
。我的测试实现在两个名为:Yakiloo.Queuing.Tests
和Yakiloo.Logging.Tests
的项目中。要获得如何使用实现类的最佳概览,您可以查看为这些类编写的测试。尽管这些测试不是为了展示应用于这些类的所有不同场景而设计的,但它们确实为您提供了它们如何工作以及应该如何使用它们的一个很好的概览。
Yakiloo.Queuing
类图
内存队列
这个类是我用作队列化工作以供不同线程处理的内存队列的实现。这使我能够重用线程,而不必创建新的线程,而我们都知道创建一个新线程是一个非常耗时的操作。内存队列允许快速写入和快速读取,并且是线程安全的。它使用了.NET Framework的System.Collections.Generic
命名空间中的通用LinkedList
。这样做的原因在于LinkedList的实现方式,它允许我们通过使用“First
”和“Last
”等属性以及允许我们向列表中添加项目的“AddLast
”等方法来轻松访问列表中的项目。如果您开始深入研究LinkedList的实现,您会发现列表中包含的项目并不是您添加的实际类型,而是为异步读写而优化的项目,而不会影响列表的其余部分。我建议您自己阅读有关该类型的信息,以更好地理解LinkedList与其他列表或集合的区别。有关LinkedList的更多信息,您可以在MSDN页面此处阅读。您应该注意的下一个项目是我如何使用System.Threading.Monitor
类以及我如何使用我实现的各种锁定对象。Monitor
类允许我做各种事情,您可以在MSDN页面此处了解。这是一个非常强大的类,如果您不熟悉多线程,我建议您阅读Sacha Barber关于多线程的文章,他在解释您在实现中需要了解的各种内容方面做得非常出色。您可以在此处找到他的第一篇文章此处,我建议您在对这些强大概念感兴趣时,首先阅读这篇文章。他非常出色地解释了如何入门以及更高级的主题是如何工作的。
线程队列
因此,我需要一种方法将一些工作传递给不同的线程,以便在它们有空闲时进行处理。借助内存队列和命令模式,我能够编写我将在本节中解释的线程队列。这个类以其工作方式封装了内存队列,它允许您将要处理的项目添加到内存队列中,但是通过命令模式,您可以编写专门的类,即应用于这些项目的命令。这意味着您无法从队列中读取,因为队列包含从队列读取并执行分配给队列的命令的线程。如果您想以更直观的方式思考,可以查看下图
在此图中,您将看到我们有多个业务流程(BP)将项目添加到队列中,以便命令对象处理。一旦进入队列,您将看到Monitor.Pulse()
方法被执行,这会让第一个等待的线程知道有工作要做。此时,等待的线程将从等待模式中出来,从队列中取出下一个第一个项目,并通过执行命令对象的Execute
方法来处理它,将工作项作为参数传递给Execute
方法。下图更清晰地展示了这一点,希望如此
正如您所看到的,有多个线程执行了Dequeue
方法,它们将处于Wait
状态,直到收到工作已完成的信号,或者调用了Dispose
方法,在这种情况下,这些线程将退出。这种实现允许我们集中处理工作项或在工作项上执行命令的方式。例如,我创建了一个Logging
项目,我将工作项分配给Logger
进行记录。我将在下一节中介绍。
Yakiloo.Logging
概述
正如我们(希望)都知道的,日志记录是编写服务和代码的一项基本工作。我需要的是一个能够写入平面文件而不会过多影响我业务流程的日志记录器,并且也不会在业务流程的执行时间上增加太多开销,即我需要一些轻量级且超快的工具。我还希望能够“插入”尽可能多的不同日志记录适配器。所以,我决定写一个小的日志框架,它允许我为它编写多个“适配器”或命令类型,并将日志写入不同的线程,这样就不会影响我的业务流程的执行时间。通过使用我在上一节中解释的线程队列,这变成了一个非常简单的任务。我所需要做的就是编写Logger
类,传入我想要记录到的线程队列,并让它处理多线程、文件访问以及当多个线程尝试写入同一个文件时我们通常会遇到的所有其他问题。在这篇文章中,这变成了一个很好的小示例应用程序,用于展示如何使用线程队列执行命令类型的能力。
类图
文件日志命令
这个命令类实现了ICommand
接口,这允许我们在Logger
类中实例化我们使用的线程队列。这是一个专门用于将数据写入文件磁盘的类的简单实现。它处理文件访问,并在达到日志设置类中看到的缓冲区大小限制后压缩文件。我使用了SharpZibLib库来为我完成压缩工作,如果您查看测试,您会发现我在日志记录了10000行文件后会压缩文件。
Logger
Logger
类是在大多数情况下用于将工作分配给队列的类。它有一个简单的Log
方法,该方法接受一个实现ILogItem
的参数。所有Log
方法所做的就是通过调用Enqueue
方法将ILogItem
添加到队列中,线程队列中的一个线程会接收它,然后通过调用FileLogCommand
类的Execute
方法来处理它。
辅助类
我在项目中使用了两个实用类,一个用于DateTime
格式化,一个用于压缩文件。我不会详细解释这些,因为我认为它们非常简单明了。
一些指标
我将在这里保持高层,以便您可以自己进行度量,但如果您查看我的测试,您会看到我为负载测试有两个测试,当然,这绝不是一个好的性能测试标准。
测试用例 1 - LoadTestLog
Number of Lines to File: 1,000,000
Zip Buffer Size: 10,000
Number of Line per Second: 100,000+ lines
Number of Line per Milli-Second: 85+ lines
Number of Threads: 1
测试用例 2 - MultiThreadedLoadTestLog
Number of Lines to File per Thread: 100,000
Zip Buffer Size: 10,000
Number of Line per Second: 85,000+ lines
Number of Line per Milli-Second: 85+ lines
Number of Threads: 5
测试 PC 标准
CPU: Intel Core i5 760
Memory: 8GB
结论
如果您查看上面显示的指标,并看到通过使用本文所示的线程队列和内存队列来实现这个小型日志框架的便捷性。我希望您同意,这可以使生活变得更加轻松,并且是在不实现非常沉重的框架从而成倍增加代码中的失败点的情况下实现异步处理的简单方法。
有用链接
- 初学者多线程指南 – Sacha Barber
ThreadingDotNet.aspx - Monitor 类 – MSDN
http://msdn.microsoft.com/en-us/library/system.threading.monitor%28v=vs.71%29.aspx - LinkedList – MSDN
http://msdn.microsoft.com/en-us/library/he2s3bh7.aspx
结论
我希望您喜欢这篇文章,就像我喜欢写它一样。我很快就会写一些关于其他概念的文章,如果您愿意,可以关注我的网站www.yakiloo.com。除了本文中解释的源代码之外,您还可以了解我将为其他文章编写的一些代码,我将在其中深入探讨一些概念,包括领域驱动设计(DDD)、测试驱动开发(TDD)和REST等。我喜欢玩概念和想法,并希望在证明这些想法时获得您的反馈。作为其中的一部分,您还应该可以在我的网站上的其他文章中找到内存队列和线程队列的其他实现,以及我如何使用它们。如果您希望我添加更多解释或关于如何使用这些类来实现您自己的东西的示例,欢迎您给我发电子邮件或留下评论,我很快就会回复您。如果您喜欢这篇文章,请不要忘记为我投票。 :-)