线程间的消费队列
线程间的生产者消费者模式举例
- 多个线程或任务可同时向集合添加项,如果集合达到其指定最大容量,则制造线程将发生阻塞,直到移除集合中的某个项。
- 多个使用者可以同时移除项,如果集合变空,则使用线程将发生阻塞,直到制造者添加某个项。
制造线程可调用 CompleteAdding 来指示不再添加项。使用者将监视 IsCompleted 属性以了解集合何时为空且不再添加项。
sealed class MessageBlockCollection
{
public event Action<string> Trigger;
private BlockingCollection<string> boundedCollection;
private Thread readThread;
public MessageBlockCollection()
{
boundedCollection = new BlockingCollection<string>(2);
}
public void SendMessage(string message)
{
// 添加单个元素,当集合已达到其BoundedCapacity时,会阻塞直到有空间可用
boundedCollection.Add(message);
}
public void ReceiveMessage()
{
try
{
readThread = new Thread(() =>
{
while (true)
{
var message = boundedCollection.Take();
Trigger?.Invoke(message);
Console.WriteLine();
}
});
readThread.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
高性能Channel
非阻塞的队列,
- 多个线程或任务可同时向集合添加项,如果集合达到其指定最大容量,则制造线程不会阻塞。
- 多个使用者可以同时移除项,如果集合变空,则使用线程也不会阻塞。
sealed class MessageChannel
{
public event Action<string> Trigger;
private Channel<string> boundedChannel;
public MessageChannel()
{
boundedChannel = Channel.CreateBounded<string>(2);
}
public async Task SendMessageAsync(string message, CancellationToken token)
{
await boundedChannel.Writer.WriteAsync(message, token);
}
public async Task ReceiveMessageAsync(CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
var message = await boundedChannel.Reader.ReadAsync(token);
Trigger?.Invoke(message);
Console.WriteLine();
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
线程间通信使用BlockingCollection比使用带互斥锁的变量优点在哪里
线程间通信使用 BlockingCollection 类相比直接使用带有互斥锁(如 lock 关键字)的变量有以下几个显著优点:
线程安全:
BlockingCollection 内部已经实现了线程安全机制,它在添加和移除元素时会自动处理同步问题,开发者无需手动编写复杂的锁代码来保证线程安全。
阻塞和唤醒机制:
当生产者尝试向满的集合添加元素或消费者尝试从空的集合取出元素时,BlockingCollection 会自动使线程进入阻塞状态,直到有空间可供添加或有元素可供取出时再自动唤醒线程。这极大地简化了线程同步逻辑,避免了“忙等待”(busy-waiting),提高了CPU使用效率。
资源管理:
它提供了对集合容量的管理,可以设置最大容量并支持是否允许集合增长。同时,它还支持完成信号,当所有元素添加完毕并且不再接受新元素时,可以通过调用 CompleteAdding() 告知消费者集合即将关闭,消费者可以根据此信号优雅地退出循环。
批量操作:
BlockingCollection 提供了如 GetConsumingEnumerable() 这样的方法,使得消费者可以直接遍历并消耗集合中的元素,直到集合为空或达到完成状态。这种方式非常适合批量处理和连续消费场景。
性能优化:
内置了多种集合类型(如 ConcurrentQueue 或 ConcurrentStack)的支持,针对不同的应用场景(比如先进先出或后进先出等)进行了性能上的优化。
减少编程错误:
直接使用锁容易导致死锁、活锁或优先级反转等问题,而使用 BlockingCollection 则隐藏了这些底层细节,降低了出错概率。
综上所述,BlockingCollection 是一个高度优化且易于使用的线程间通信工具,特别适用于生产者-消费者模式
的实现,相比简单的互斥锁机制更加健壮、高效且易用。
BlockingCollection跟ConcurrentQueue的关系
BlockingCollection 和 ConcurrentQueue 都是.NET框架中用于多线程环境下的线程安全集合,但是它们在功能和用途上有明显的区别和优劣:
BlockingCollection:
功能:BlockingCollection 是一个更高级别的容器,它封装了一个基础集合(如 ConcurrentQueue 或 ConcurrentStack),并提供了阻塞的添加和移除功能。这意味着当试图从空集合中取出元素时,消费者线程会自动阻塞,直到有元素可用;当集合达到最大容量时,生产者线程也会阻塞,直到有空间可以添加元素。
特点:支持设置集合的最大容量限制,并提供了 CompleteAdding() 方法来标识不再添加新元素,消费者线程可通过 IsCompleted 属性检测是否所有的元素都已经添加完毕且不会再有新的添加操作。
优点:简化了多线程间的同步,提供了生产者-消费者模式的完整解决方案,包括线程安全、阻塞和容量管理等功能。
应用场景:适用于生产者和消费者之间需要明确同步和容量控制的场景,以及希望简化多线程同步代码的情况。
ConcurrentQueue:
功能:ConcurrentQueue 是一个线程安全的先进先出(FIFO)队列,允许多个线程同时进行入队和出队操作而不引发异常。
特点:没有内置的阻塞机制,所以即使队列为空,消费者也不会阻塞;相反,它会立刻返回一个无法获取元素的状态。同样,当队列满时,生产者仍然可以继续尝试添加元素,但并不会因为队列满而阻塞。
优点:对于高性能的多线程环境,ConcurrentQueue 提供了非阻塞式的快速入队和出队操作,适合于性能敏感且可以容忍一定延迟的场景。
缺点:由于缺少阻塞功能,开发者需要自己实现更复杂的同步逻辑来处理队列为空或满的情况,比如使用 Monitor.Pulse 和 Monitor.Wait,或者配合 ManualResetEventSlim、SemaphoreSlim 等同步原语。
应用场景:适用于大量并发插入和删除,且不需要精确同步生产者和消费者进度的场景。
结论: 如果你需要一个简单易用的完全封装了生产者-消费者模式的集合,那么 BlockingCollection 是首选,因为它简化了并发编程的复杂度,提供了自动的阻塞和容量控制功能。
而如果你关注的是绝对的性能和灵活性,或者是需要自定义同步逻辑,那么 ConcurrentQueue 更合适,但你必须自己处理好线程间的同步问题。在一些特定的高性能场景中,非阻塞的 ConcurrentQueue 可能会带来更好的性能表现。