问题

C# ConcurrentQueue 怎么长时间循环取任务?

回答
好的,我们来聊聊 C 中 `ConcurrentQueue` 的长时间循环取任务这个话题。我会尽量用一种大家都能理解的方式来讲解,就像是跟朋友们一起探讨技术一样,而不是生硬的教科书式回答。

想象一下,你有一个流水线,上面有很多待加工的零件(也就是我们的任务)。你需要一个不知疲倦的工人(我们的循环),不停地从流水线上拿起零件,然后进行加工。这个工人就是我们要实现的“长时间循环取任务”。

为什么需要 `ConcurrentQueue`?

在开始讲解“怎么做”之前,咱们先说说“为什么”。在多线程的环境下,多个线程可能同时需要访问同一个数据结构,比如一个任务队列。如果你用一个普通的 `List` 或者 `Queue`,当两个线程几乎同时去添加或移除任务时,就可能会出现问题,比如数据损坏、索引错误等等。这就像流水线上的工人,如果两个人同时去拿同一个零件,很容易发生碰撞或者错拿。

这时候,`ConcurrentQueue` 就派上用场了。它是一个“线程安全”的队列。这意味着你可以放心地让多个线程同时往里面放任务,或者从里面取任务,而不用担心出现上面提到的那些糟糕的情况。它内部做了很多精妙的设计,保证了操作的原子性和一致性,让你的代码更健壮。

长时间循环取任务:最直接的思路

最简单直接的想法是:在一个无限循环里不停地尝试从队列里取任务。

```csharp
using System;
using System.Collections.Concurrent;
using System.Threading;

public class TaskProcessor
{
private ConcurrentQueue _taskQueue = new ConcurrentQueue();

// 添加任务的方法
public void AddTask(string task)
{
_taskQueue.Enqueue(task);
Console.WriteLine($"添加任务:{task}");
}

// 启动任务处理循环
public void StartProcessing()
{
// 使用一个布尔值来控制循环是否继续,方便后续优雅地停止
bool keepProcessing = true;

while (keepProcessing)
{
// 尝试从队列中取出一个任务
if (_taskQueue.TryDequeue(out string task))
{
// 成功取到任务,进行处理
Console.WriteLine($"处理任务:{task}");
// 模拟任务处理耗时
Thread.Sleep(100);
}
else
{
// 队列为空,暂时没有任务可处理
// 这里很重要!如果直接在这个else里什么都不做,循环会以极高的CPU占用率空转
// 我们需要让出CPU时间给其他线程
Thread.Sleep(10); // 稍微等待一下,避免CPU空转
}
}
}

// 停止处理循环的方法
public void StopProcessing()
{
// 在实际应用中,你可能需要一个更复杂的机制来确保所有已取出但未处理的任务完成
// 或者允许在当前处理的任务完成后停止。
// 这里我们只是简单地设置标志位,让循环在下次检查时退出。
// keepProcessing = false; // 如果 StartProcessing 方法是实例方法且 keepProcessing 是实例字段,可以这样做。
// 如果是静态方法,则需要一个静态字段或者更复杂的共享变量机制。
Console.WriteLine("停止任务处理循环...");
// 如果需要等待当前正在处理的任务完成,需要更复杂的同步机制。
}

public static void Main(string[] args)
{
TaskProcessor processor = new TaskProcessor();

// 模拟启动一个处理线程
Thread processingThread = new Thread(() => processor.StartProcessing());
processingThread.IsBackground = true; // 设置为后台线程,程序退出时会自动终止
processingThread.Start();

// 模拟添加一些任务
for (int i = 1; i <= 5; i++)
{
processor.AddTask($"任务 {i}");
Thread.Sleep(50); // 间隔一点时间添加任务
}

// 等待一段时间,让处理线程有机会工作
Thread.Sleep(5000);

// 模拟再添加一些任务
for (int i = 6; i <= 10; i++)
{
processor.AddTask($"任务 {i}");
Thread.Sleep(50);
}

// 这里我们让主线程继续运行一段时间,观察处理效果
Thread.Sleep(5000);

// 在实际应用中,如果需要优雅地停止,你需要一个更完善的停止机制
// processor.StopProcessing();
// processingThread.Join(); // 等待处理线程结束

Console.WriteLine("主线程结束,程序退出。");
}
}
```

深入剖析上面的代码,以及一些需要注意的点:

1. `ConcurrentQueue.TryDequeue(out T result)`: 这是我们获取任务的核心方法。它非常智能。如果队列中有任务,它会立即尝试取出一个并放入 `result`,然后返回 `true`。如果队列是空的,它不会等待,而是立刻返回 `false`。这很重要,因为我们不希望循环卡死在等待任务上。

2. `while (true)` 的陷阱(以及如何避免): 如果你直接写 `while (true)` 并且在队列为空时什么都不做,你的那个处理任务的线程就会以 100% 的 CPU 利用率在那里“空转”。这就像一个人不停地查看一个空空的邮箱,一点都不节能,而且还会影响到其他需要 CPU 的程序。
所以,在 `else` 分支(即队列为空)时,我们通常会加入一个 短暂的暂停 (`Thread.Sleep(milliseconds)`)。这个 `Sleep` 的时间需要权衡:
太短(比如 0 或 1ms):CPU 占用率仍然可能很高。
太长:新任务来了之后,处理线程响应就会变慢。
通常,10ms 到 50ms 是一个比较常见的选择,具体取决于你的任务响应需求和对 CPU 占用的容忍度。

3. 线程管理(`Thread.IsBackground = true`): 在示例中,我们将处理任务的线程设置为 `IsBackground = true`。这意味着一旦所有非后台线程(比如我们的 `Main` 方法线程)都结束了,即使这个处理线程还在运行,整个程序也会退出。这在很多场景下是方便的,但也意味着你可能无法“优雅地”关闭它。如果你需要控制程序的关闭流程,让它处理完当前任务再退出,就需要更复杂的同步机制(比如 `CancellationToken` 或一个专门的停止标志)。

4. `Thread.Sleep` 的替代方案(更“现代”的等待): 虽然 `Thread.Sleep` 是最简单的,但在某些情况下,我们可能希望等待得更“智能”一些。比如,当队列为空时,我们希望线程能够“挂起”,直到有新的任务被添加进来,这时才被唤醒。这就需要更高级的同步原语,比如:
`BlockingCollection`: 这是 C 提供的一个更高级的集合类型,它内置了阻塞操作。当你从 `BlockingCollection` 中取元素时,如果集合为空,它会自动阻塞当前线程,直到有元素被添加。当有元素添加进来时,它会自动唤醒等待的线程。这通常是处理这种“有任务就干,没任务就等”场景的最佳选择。
`ManualResetEventSlim` 或 `AutoResetEvent`: 你可以结合 `ConcurrentQueue` 和一个事件对象来模拟阻塞。当队列为空时,线程等待事件。当有新任务添加时,将任务放入队列,然后触发事件,唤醒等待的线程。这稍微复杂一些,但能提供更细粒度的控制。

使用 `BlockingCollection`:更推荐的优雅方式

鉴于 `BlockingCollection` 的优势,我们强烈推荐在大多数需要“长时间循环取任务”的场景中使用它。它帮你处理了“等待”和“唤醒”的逻辑,让你的代码更简洁、更高效。

下面是使用 `BlockingCollection` 的例子:

```csharp
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks; // 使用 Task 代替 Thread 更加方便

public class BlockingTaskProcessor
{
// BlockingCollection 是线程安全的,并提供了阻塞操作
private BlockingCollection _taskCollection = new BlockingCollection();

// 添加任务
public void AddTask(string task)
{
_taskCollection.Add(task); // Add 方法会在集合已满时阻塞(如果设置了容量上限)
Console.WriteLine($"添加任务:{task}");
}

// 启动任务处理循环
public void StartProcessing()
{
Console.WriteLine("任务处理器启动...");

// ConsumeRemotely() 会一直循环,直到 _taskCollection.CompleteAdding() 被调用
// 并且集合变为空。
// 这是一个非常优雅的方式来处理循环的生命周期。
foreach (var task in _taskCollection.GetConsumingEnumerable())
{
Console.WriteLine($"处理任务:{task}");
// 模拟任务处理耗时
Thread.Sleep(100);
}

Console.WriteLine("任务处理器停止,队列已空。");
}

// 标记集合添加完成,并允许当前所有已取出任务完成后退出循环
public void StopProcessing()
{
Console.WriteLine("通知任务处理器停止...");
_taskCollection.CompleteAdding(); // 告诉 BlockingCollection,不会再有新的任务添加了。
}

public static async Task Main(string[] args)
{
BlockingTaskProcessor processor = new BlockingTaskProcessor();

// 启动一个处理任务的 Task
// Task.Run(() => processor.StartProcessing()); 也可以
Task processingTask = Task.Factory.StartNew(() => processor.StartProcessing(), TaskCreationOptions.LongRunning);


// 模拟添加一些任务
for (int i = 1; i <= 5; i++)
{
processor.AddTask($"任务 {i}");
await Task.Delay(50); // 使用 Task.Delay 替代 Thread.Sleep
}

// 等待一段时间,让处理线程有机会工作
await Task.Delay(2000);

// 模拟再添加一些任务
for (int i = 6; i <= 10; i++)
{
processor.AddTask($"任务 {i}");
await Task.Delay(50);
}

// 完成添加任务,并等待处理完所有任务后,处理线程会自动退出
processor.StopProcessing();

// 等待处理任务的 Task 完成
await processingTask;

Console.WriteLine("主线程结束,程序退出。");
}
}
```

使用 `BlockingCollection` 的优势:

1. 自动阻塞和唤醒: `BlockingCollection` 在你尝试从空集合中取元素时,会自动挂起线程,并且在有新元素添加时唤醒它。这比 `ConcurrentQueue`+`Thread.Sleep` 的方式更有效率,也更省 CPU。
2. 优雅的停止机制 (`CompleteAdding()` 和 `GetConsumingEnumerable()`): `CompleteAdding()` 方法标志着集合的添加过程已经结束。`GetConsumingEnumerable()` 方法会持续不断地从集合中获取元素,直到集合被标记为 `CompleteAdding()` 并且集合本身为空时,这个 `foreach` 循环才会自然结束。这提供了一个非常干净、优雅的方式来控制处理循环的生命周期。
3. 容量限制(可选): `BlockingCollection` 允许你指定一个容量上限。如果集合满了,尝试添加元素的操作会阻塞,直到有元素被取出。这对于限制内存使用或避免生产方(添加任务的线程)过快而消费方(处理任务的线程)过慢导致内存溢出非常有用。

总结一下:

如果你只是简单地想在后台线程里不停地轮询一个 `ConcurrentQueue`,并且对 CPU 占用率要求不那么苛刻,那么在队列为空时加一个 `Thread.Sleep` 是可以工作的。

但是,如果你追求更高效、更优雅、更符合现代 C 编程习惯的解决方案,强烈建议使用 `BlockingCollection`。 它内置了处理这种生产者消费者模式的逻辑,让你能更专注于任务本身的逻辑,而不是底层线程同步的细节。

希望我讲得够详细,也希望能帮大家理清思路。在实际项目中,根据你的具体需求和对系统性能的要求来选择最合适的方法就好!

网友意见

user avatar

可以考虑用Channel,有async,降低cpu空转


类似的话题

  • 回答
    好的,我们来聊聊 C 中 `ConcurrentQueue` 的长时间循环取任务这个话题。我会尽量用一种大家都能理解的方式来讲解,就像是跟朋友们一起探讨技术一样,而不是生硬的教科书式回答。想象一下,你有一个流水线,上面有很多待加工的零件(也就是我们的任务)。你需要一个不知疲倦的工人(我们的循环),不.............
  • 回答
    在 C++ 中,循环内部定义与外部同名变量不报错,是因为 作用域(Scope) 的概念。C++ 的作用域规则规定了变量的可见性和生命周期。我们来详细解释一下这个过程:1. 作用域的定义作用域是指一个标识符(变量名、函数名等)在程序中可以被识别和使用的区域。C++ 中的作用域主要有以下几种: 文件.............
  • 回答
    C 语言的设计理念是简洁、高效、接近硬件,而其对数组的设计也遵循了这一理念。从现代编程语言的角度来看,C 语言的数组确实存在一些“不改进”的地方,但这些“不改进”很大程度上是为了保持其核心特性的兼容性和效率。下面我将详细阐述 C 语言为何不“改进”数组,以及这种设计背后的权衡和原因:1. 数组在 C.............
  • 回答
    C 语言王者归来,原因何在?C 语言,这个在编程界已经沉浮数十载的老将,似乎并没有随着时间的推移而消逝,反而以一种“王者归来”的姿态,在许多领域焕发新生。它的生命力如此顽强,甚至在 Python、Java、Go 等语言层出不穷的今天,依然占据着不可动摇的地位。那么,C 语言究竟为何能实现“王者归来”.............
  • 回答
    C罗拒绝同框让可口可乐市值下跌 40 亿美元,可口可乐回应「每个人都有不同的口味和需求」,这件事可以说是近几年体育界和商业界结合的一个典型案例,也引发了很多的讨论和思考。我们来详细地分析一下:事件本身: 核心行为: 在2021年欧洲杯小组赛葡萄牙对阵匈牙利的赛前新闻发布会上,葡萄牙球星克里斯蒂亚.............
  • 回答
    C++20 的协程(coroutines)和 Go 的 goroutines 都是用于实现并发和异步编程的强大工具,但它们的设计理念、工作方式以及适用的场景有显著的区别。简单地说,C++20 协程虽然强大且灵活,但与 Go 的 goroutines 在“易用性”和“轻量级”方面存在较大差距,不能完全.............
  • 回答
    在 C++ 中,为基类添加 `virtual` 关键字到析构函数是一个非常重要且普遍的实践,尤其是在涉及多态(polymorphism)的场景下。这背后有着深刻的内存管理和对象生命周期管理的原理。核心问题:为什么需要虚析构函数?当你在 C++ 中使用指针指向一个派生类对象,而这个指针的类型是基类指针.............
  • 回答
    在 C/C++ 中,采用清晰的命名规则是编写可维护、易于理解和协作代码的关键。一个好的命名规范能够让其他开发者(包括未来的你)快速理解代码的意图、作用域和类型,从而提高开发效率,减少 Bug。下面我将详细阐述 C/C++ 中推荐的命名规则,并提供详细的解释和示例。核心原则:在深入具体规则之前,理解这.............
  • 回答
    C++之所以没有被淘汰,尽管其被普遍认为“复杂”,其原因绝非单一,而是由一系列深刻的历史、技术和生态系统因素共同作用的结果。理解这一点,需要深入剖析C++的定位、优势、以及它所代表的工程哲学。以下是详细的解释: 1. 历史的沉淀与根基的稳固 诞生于C的土壤: C++并非凭空出现,它是对C语言的强.............
  • 回答
    C++ 模板:功能强大的工具还是荒谬拙劣的小伎俩?C++ 模板无疑是 C++ 语言中最具争议但也最引人注目的一项特性。它既能被誉为“代码生成器”、“通用编程”的基石,又可能被指责为“编译时地狱”、“难以理解”的“魔法”。究竟 C++ 模板是功能强大的工具,还是荒谬拙劣的小伎俩?这需要我们深入剖析它的.............
  • 回答
    C 语言本身并不能直接“编译出一个不需要操作系统的程序”,因为它需要一个运行环境。更准确地说,C 语言本身是一种编译型语言,它将源代码转换为机器码,而机器码的执行是依赖于硬件的。然而,当人们说“不需要操作系统的程序”时,通常指的是以下几种情况,而 C 语言可以用来实现它们:1. 嵌入式系统中的裸机.............
  • 回答
    C++ 中实现接口与分离(通常是通过抽象类、纯虚函数以及对应的具体类)后,确实会增加文件的数量,这可能会让人觉得“麻烦”。但这种增加的文件数量背后,隐藏着巨大的好处,使得代码更加健壮、灵活、可维护和可扩展。下面我将详细阐述这些好处:核心思想:解耦 (Decoupling)接口与实现分离的核心思想是解.............
  • 回答
    C++ 是一门强大而灵活的编程语言,它继承了 C 语言的高效和底层控制能力,同时引入了面向对象、泛型编程等高级特性,使其在各种领域都得到了广泛应用。下面我将尽可能详细地阐述 C++ 的主要优势: C++ 的核心优势:1. 高性能和底层控制能力 (Performance and LowLevel C.............
  • 回答
    C语言指针是否难,以及数学大V认为指针比范畴论还难的说法,是一个非常有趣且值得深入探讨的话题。下面我将尽量详细地阐述我的看法。 C语言指针:理解的“门槛”与“终点”首先,我们需要明确“难”的定义。在编程领域,“难”通常指的是: 学习曲线陡峭: 需要花费大量时间和精力去理解和掌握。 容易出错:.............
  • 回答
    在 C/C++ 中,指针声明的写法确实存在两种常见的形式:`int ptr;` 和 `int ptr;`。虽然它们最终都声明了一个指向 `int` 类型的指针变量 `ptr`,但它们在语法上的侧重点和历史演变上有所不同,导致了后者(`int ptr;`)更为普遍和被推荐。下面我将详细解释为什么通常写.............
  • 回答
    C++ 的核心以及“精通”的程度,这是一个非常值得深入探讨的话题。让我尽量详细地为您解答。 C++ 的核心究竟是什么?C++ 的核心是一个多层次的概念,可以从不同的角度来理解。我将尝试从以下几个方面来阐述:1. 语言设计的哲学与目标: C 的超集与面向对象扩展: C++ 最初的目标是成为 C 语.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    在 C/C++ 编程中,确实存在一些写法,它们本身可能不是最优的解决方案,甚至在大多数情况下是多余的,但却能让有一定经验的开发者眼前一亮,感到“不明觉厉”。这些写法往往巧妙地利用了语言的特性、预处理指令、或者是一些不常用的语法糖。同时,它们又不会像一些“炫技”般的操作那样显得过于怪异而难以理解。下面.............
  • 回答
    在C/C++中,当您声明一个 `int a = 15;` 这样的局部变量时,它通常存储在 栈 (Stack) 上。下面我们来详细解释一下,并涉及一些相关的概念:1. 变量的生命周期与存储区域在C/C++中,变量的存储位置取决于它们的生命周期和作用域。主要有以下几个存储区域: 栈 (Stack):.............
  • 回答
    「C++ 早就过时了,大部分写工程不用 C++,学习这个语言只是为了竞赛」这个观点并不完全正确,而且存在很大的片面性。虽然C++在某些领域的使用有所下降,并且确实在竞赛领域非常流行,但它在现代工程领域仍然扮演着至关重要的角色,并且远未“过时”。下面我将从多个角度来详细阐述为什么这个观点是错误的,以及.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有