线程安全实现方式对比
使用线程安全的集合
线程安全的集合类都位于System.Collections.Concurrent命名空间中
-
ConcurrentBag<T>
:表示一个线程安全的无序集合,允许并发添加和移除元素。它不同于传统的集合,因为它不保证元素的顺序,并且不支持枚举过程中的元素修改。ConcurrentBag<T>
特别适合用于生产者-消费者场景,其中多个线程可能同时向集合中添加或移除元素。 -
ConcurrentQueue<T>
:表示一个线程安全的FIFO(先进先出)集合。它支持并发的出队和入队操作。与ConcurrentBag<T>
不同,ConcurrentQueue<T>
保证了元素的FIFO顺序。 -
ConcurrentStack<T>
:表示一个线程安全的LIFO(后进先出)集合。它支持并发的推入和弹出操作,类似于传统的栈数据结构。 -
ConcurrentDictionary<TKey, TValue>
:表示一个线程安全的键值对集合,支持并发的添加、访问、更新和删除操作。这个集合类非常有用,当你需要在多线程环境中存储和检索键值对时,可以使用它。 -
BlockingCollection<T>
:这是一个特殊的线程安全集合,它提供了阻塞和界限功能。当集合为空时,尝试从集合中获取元素的线程会被阻塞,直到有元素可用为止。同样,当集合达到其界限时,尝试向集合中添加元素的线程也会被阻塞。这使得BlockingCollection<T>
非常适合用于生产者-消费者模式,其中生产者线程向集合中添加元素,而消费者线程从集合中移除元素。 -
ConcurrentLinkedQueue<T>
和ConcurrentLinkedDeque<T>
:这些是 .NET Core 中引入的集合,分别提供了线程安全的队列和双端队列实现。它们基于链接节点,因此不需要预先分配固定大小的数组。 -
AsyncCollection<T>
:这不是.NET标准库的一部分,但可以在一些第三方库或用户自定义代码中实现。AsyncCollection<T>
通常用于异步编程模型,允许以异步方式添加和移除元素。 -
在C#中,特别是在.NET Core和.NET 5+中,
System.Threading.Channels
命名空间提供了一个Channel<T>
类型,它提供了基于通道的异步消息传递功能。Channel<T>
类允许生产者线程向通道写入消息,消费者线程从通道读取消息。通道是线程安全的,并且支持异步操作,因此它们是并发编程中的强大工具。using System; -
using System.Threading.Channels; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // 创建一个具有默认容量的通道 var channel = Channel.CreateUnbounded<int>(); // 生产者任务 var producerTask = Task.Run(async () => { for (int i = 0; i < 10; i++) { // 将消息写入通道 await channel.Writer.WriteAsync(i); Console.WriteLine($"Produced: {i}"); } // 标记通道完成写入,告诉消费者没有更多的消息了 channel.Writer.Complete(); }); // 消费者任务 var consumerTask = Task.Run(async () => { await foreach (var item in channel.Reader.ReadAllAsync()) { // 从通道读取消息 Console.WriteLine($"Consumed: {item}"); } // 当通道完成读取时,ReadAllAsync 会完成 Console.WriteLine("All messages consumed."); }); // 等待生产者和消费者任务完成 await Task.WhenAll(producerTask, consumerTask); } }
同步机制
1、原子操作 InterLocked
2、信号量
3、线程锁
原子操作和锁在多线程编程中都是重要的同步机制,但它们之间存在显著的区别。以下是对两者区别的清晰归纳:
一、定义与性质
-
原子操作:原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就会一直运行到结束,中间不会有任何context switch(即不会切换到另一个线程)。它是多个操作不可被打乱或切割的整体,具有不可分割性。
-
锁:锁是一种并发控制机制,用于保护共享资源,防止多个线程同时访问或修改同一个资源,从而避免数据不一致或产生竞态条件。锁提供了互斥访问的机制,确保在某一时刻只有一个线程能够获取锁并执行临界区代码。
二、执行效率与开销
-
原子操作:原子操作通常比锁执行得更快,因为它不会引起上下文切换,从而减少了开销。此外,原子操作通常由底层硬件或处理器直接支持,这进一步提高了其执行效率。
-
锁:相比之下,锁的使用通常会导致上下文切换,因为当一个线程试图获取已被其他线程持有的锁时,它会被阻塞并进入等待状态。这种上下文切换带来了额外的开销,并可能降低程序的总体性能。
三、资源占用与释放
-
原子操作:在执行过程中,原子操作会一直占用资源并等待条件满足。它不会被其他任务或事件中断,因此不需要释放资源。
-
锁:当一个线程获取锁并执行完临界区代码后,它会释放锁,从而允许其他线程获取锁并访问共享资源。这种机制确保了资源的合理利用和并发访问的安全性。
四、使用场景与灵活性
-
原子操作:原子操作通常用于简单的、短小的临界区代码,以及对性能要求极高的场景。由于原子操作的不可分割性,它非常适合于确保某些关键操作的原子性和一致性。
-
锁:锁提供了更高级别的并发控制,适用于更复杂的临界区代码和需要更精细控制并发访问的场景。锁还支持可重入性、可中断性和公平性等特性,为开发者提供了更多的灵活性和控制选项。
综上所述,原子操作和锁在定义、执行效率、资源占用与使用场景等方面存在显著差异。在选择使用哪种机制时,开发者应根据具体需求和场景进行权衡和选择。