- C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
- (美)Christian Nagel
- 1611字
- 2025-02-18 01:49:56
11.4 队列
队列是其元素以先进先出(Firstin, Firstout, FIFO)的方式来处理的集合。先放入队列中的元素会先读取。队列的例子有在机场排的队列、人力资源部中等待处理求职信的队列和打印队列中等待处理的打印任务,以及按循环方式等待CPU处理的线程。另外,还常常有元素根据其优先级来处理的队列。
例如,在机场的队列中,商务舱乘客的处理要优先于经济舱的乘客。这里可以使用多个队列,一个队列对应一个优先级。在机场,这很常见,因为商务舱乘客和经济舱乘客有不同的登记队列。打印队列和线程也是这样。可以为一组队列建立一个数组,数组中的一项代表一个优先级。在每个数组项中都有一个队列,其中按照FIFO的方式进行处理。
注意:本章的后面将使用链表的另一种实现方式来定义优先级列表。
队列使用System.Collections.Generic名称空间中的泛型类Queue<T>实现。在内部,Queue<T>类使用T类型的数组,这类似于List<T>类型。它实现ICollection和IEnumerable<T>接口,但没有实现ICollection<T>接口,因为这个接口定义的Add()和Remove()方法不能用于队列。
因为Queue<T>类没有实现IList<T>接口,所以不能用索引器访问队列。队列只允许在队列中添加元素,该元素会放在队列的尾部(使用Enqueue()方法),从队列的头部获取元素(使用Dequeue()方法)。
图11-1显示了队列的元素。Enqueue()方法在队列的一端添加元素,Dequeue()方法在队列的另一端读取和删除元素。再次调用Dequeue()方法,会删除队列中的下一项。

图11-1
Queue<T>类的方法如表11-2所示。
表11-2

在创建队列时,可以使用与List<T>类型类似的构造函数。虽然默认的构造函数会创建一个空队列,但也可以使用构造函数指定容量。在把元素添加到队列中时,如果没有定义容量,容量就会递增,从而包含4、8、16和32个元素。类似于List<T>类,队列的容量也总是根据需要成倍增加。非泛型类Queue的默认构造函数与此不同,它会创建一个包含32项空的初始数组。使用构造函数的重载版本,还可以将实现了IEnumerable<T>接口的其他集合复制到队列中。
下面的文档管理应用程序示例说明了Queue<T>类的用法。使用一个线程将文档添加到队列中,用另一个线程从队列中读取文档,并处理它们。
存储在队列中的项是Document类型。Document类定义了标题和内容(代码文件QueueSample/Document.cs):
public class Document { public string Title { get; private set; } public string Content { get; private set; } public Document(string title, string content) { Title = title; Content = content; } }
DocumentManager类是Queue<T>类外面的一层。DocumentManager类定义了如何处理文档:用AddDocument()方法将文档添加到队列中,用GetDocument()方法从队列中获得文档。
在AddDocument()方法中,用Enqueue()方法把文档添加到队列的尾部。在GetDocument()方法中,用Dequeue()方法从队列中读取第一个文档。因为多个线程可以同时访问DocumentManager类,所以用lock语句锁定对队列的访问。
注意:线程和lock语句参见第21章和第22章。
IsDocumentAvailable是一个只读类型的布尔属性,如果队列中还有文档,它就返回true,否则返回false(代码文件QueueSample/DocumentManager.cs)。
public class DocumentManager { private readonly Queue<Document> _documentQueue = new Queue<Document>(); public void AddDocument(Document doc) { lock (this) { _documentQueue.Enqueue(doc); } } public Document GetDocument() { Document doc = null; lock (this) { doc = _documentQueue.Dequeue(); } return doc; } public bool IsDocumentAvailable => _documentQueue.Count > 0; }
ProcessDocuments类在一个单独的任务中处理队列中的文档。能从外部访问的唯一方法是Start()。在Start()方法中,实例化了一个新任务。创建一个ProcessDocuments对象,来启动任务,定义Run()方法作为任务的启动方法。TaskFactory(通过Task类的静态属性Factory访问)的StartNew方法需要一个Action委托作为参数,用于接受Run方法传递的地址。TaskFactory的StartNew方法会立即启动任务。
使用ProcessDocuments类的Run()方法定义一个无限循环。在这个循环中,使用属性IsDocumentAvailable确定队列中是否还有文档。如果队列中还有文档,就从DocumentManager类中提取文档并处理。这里的处理仅是把信息写入控制台。在真正的应用程序中,文档可以写入文件、数据库,或通过网络发送(代码文件QueueSample/ProcessDocuments.cs)。
public class ProcessDocuments { public static void Start(DocumentManager dm) { Task.Run(new ProcessDocuments(dm).Run); } protected ProcessDocuments(DocumentManager dm) { if (dm == null) throw new ArgumentNullException(nameof(dm)); _documentManager = dm; } private DocumentManager _documentManager; protected async Task Run() { while (true) { if (_documentManager.IsDocumentAvailable) { Document doc = _documentManager.GetDocument(); WriteLine("Processing document {0}", doc.Title); } await Task.Delay(new Random().Next(20)); } } }
在应用程序的Main()方法中,实例化一个DocumentManager对象,启动文档处理任务。接着创建1000个文档,并添加到DocumentManager对象中(代码文件QueueSample/Program.cs):
public class Program { public static void Main() { var dm = new DocumentManager(); ProcessDocuments.Start(dm); // Create documents and add them to the DocumentManager for (int i = 0; i < 1000; i++) { var doc = new Document($"Doc {i.ToString()}", "content"); dm.AddDocument(doc); WriteLine($"Added document {doc.Title}"); Thread.Sleep(new Random().Next(20)); } } }
在启动应用程序时,会在队列中添加和删除文档,输出如下所示:
Added document Doc 279 Processing document Doc 236 Added document Doc 280 Processing document Doc 237 Added document Doc 281 Processing document Doc 238 Processing document Doc 239 Processing document Doc 240 Processing document Doc 241 Added document Doc 282 Processing document Doc 242 Added document Doc 283 Processing document Doc 243
完成示例应用程序中描述的任务的真实程序可以处理用Web服务接收到的文档。