引言:为什么在 C# 中需要任务队列?
在现代 C# (.NET) 开发中,经常需要组织异步数据处理。一个经典的例子是 Web 服务器,它接收请求并需要在后台处理它们,而不阻塞主线程。或者一个应用程序,它从互联网下载文件并并行处理已下载的文件。对于此类场景,“生产者-消费者”模式非常理想,而其基础就是任务队列。
任务队列允许安全地将工作从一个线程(或异步操作)传递到另一个线程,同步对共享数据的访问。在本文中,我们将探讨在 C# 中实现队列的三种主要方法:ConcurrentQueue、BlockingCollection 和现代的 System.Threading.Channels。每种方法都适用于不同的场景,我们将通过实际示例进行讲解。
1. ConcurrentQueue — 基本线程安全队列
ConcurrentQueue<T> 是来自 System.Collections.Concurrent 命名空间的最简单的线程安全队列。它使用无锁算法(比较并交换,CAS)来实现高性能。但是,当队列为空时,它不会阻塞线程——您需要自行实现等待机制。
示例:从多个源加载数据
假设我们有一个任务:下载 10 个网页并处理它们。一个线程(生产者)将 URL 添加到队列中,多个线程(消费者)从队列中取出 URL 并进行下载。
using System;using System.Collections.Concurrent;using System.Threading.Tasks;
public class ConcurrentQueueExample{ public static async Task RunAsync() { var queue = new ConcurrentQueue<string>(); var urls = new[] { "https://example.com", "https://dotnet.microsoft.com" };
// Producer: 添加任务 foreach (var url in urls) { queue.Enqueue(url); }
// Consumers: 处理任务 var tasks = new Task[3]; for (int i = 0; i < tasks.Length; i++) { tasks[i] = Task.Run(() => { while (queue.TryDequeue(out var url)) { Console.WriteLine($"处理中: {url}"); // 模拟工作 Task.Delay(500).Wait(); } }); }
await Task.WhenAll(tasks); Console.WriteLine("所有任务已完成。"); }}优点:简单,在大量操作下性能高。
缺点:没有内置的等待机制(需要轮询循环),难以限制队列大小。
2. BlockingCollection — 带阻塞和限制的队列
BlockingCollection<T> 是线程安全集合的包装器,它增加了在队列为空时阻塞消费者线程,以及在队列已满时阻塞生产者线程的能力。默认情况下,它使用 ConcurrentQueue,但可以替换为任何实现 IProducerConsumerCollection<T> 的集合。
示例:处理带缓冲区限制的订单
想象一个订单处理系统。生产者生成订单,三个消费者处理它们。缓冲区限制为 10 个元素,以避免消耗过多内存。
using System;using System.Collections.Concurrent;using System.Threading.Tasks;
public class BlockingCollectionExample{ public static async Task RunAsync() { using var orders = new BlockingCollection<string>(boundedCapacity: 10);
// Producer var producer = Task.Run(() => { for (int i = 1; i <= 20; i++)