线程安全实现方式对比

使用线程安全的集合

线程安全的集合类都位于System.Collections.Concurrent命名空间中

  1. ConcurrentBag<T>:表示一个线程安全的无序集合,允许并发添加和移除元素。它不同于传统的集合,因为它不保证元素的顺序,并且不支持枚举过程中的元素修改。ConcurrentBag<T> 特别适合用于生产者-消费者场景,其中多个线程可能同时向集合中添加或移除元素。

  2. ConcurrentQueue<T>:表示一个线程安全的FIFO(先进先出)集合。它支持并发的出队和入队操作。与 ConcurrentBag<T> 不同,ConcurrentQueue<T> 保证了元素的FIFO顺序。

  3. ConcurrentStack<T>:表示一个线程安全的LIFO(后进先出)集合。它支持并发的推入和弹出操作,类似于传统的栈数据结构。

  4. ConcurrentDictionary<TKey, TValue>:表示一个线程安全的键值对集合,支持并发的添加、访问、更新和删除操作。这个集合类非常有用,当你需要在多线程环境中存储和检索键值对时,可以使用它。

  5. BlockingCollection<T>:这是一个特殊的线程安全集合,它提供了阻塞和界限功能。当集合为空时,尝试从集合中获取元素的线程会被阻塞,直到有元素可用为止。同样,当集合达到其界限时,尝试向集合中添加元素的线程也会被阻塞。这使得 BlockingCollection<T> 非常适合用于生产者-消费者模式,其中生产者线程向集合中添加元素,而消费者线程从集合中移除元素。

  6. ConcurrentLinkedQueue<T> 和 ConcurrentLinkedDeque<T>:这些是 .NET Core 中引入的集合,分别提供了线程安全的队列和双端队列实现。它们基于链接节点,因此不需要预先分配固定大小的数组。

  7. AsyncCollection<T>:这不是.NET标准库的一部分,但可以在一些第三方库或用户自定义代码中实现。AsyncCollection<T> 通常用于异步编程模型,允许以异步方式添加和移除元素。

  8. 在C#中,特别是在.NET Core和.NET 5+中,System.Threading.Channels 命名空间提供了一个Channel<T>类型,它提供了基于通道的异步消息传递功能。

    Channel<T>类允许生产者线程向通道写入消息,消费者线程从通道读取消息。通道是线程安全的,并且支持异步操作,因此它们是并发编程中的强大工具。using System;  

  9. 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、线程锁

原子操作和锁在多线程编程中都是重要的同步机制,但它们之间存在显著的区别。以下是对两者区别的清晰归纳:

一、定义与性质

  1. 原子操作:原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就会一直运行到结束,中间不会有任何context switch(即不会切换到另一个线程)。它是多个操作不可被打乱或切割的整体,具有不可分割性。

  2. 锁:锁是一种并发控制机制,用于保护共享资源,防止多个线程同时访问或修改同一个资源,从而避免数据不一致或产生竞态条件。锁提供了互斥访问的机制,确保在某一时刻只有一个线程能够获取锁并执行临界区代码。

二、执行效率与开销

  1. 原子操作:原子操作通常比锁执行得更快,因为它不会引起上下文切换,从而减少了开销。此外,原子操作通常由底层硬件或处理器直接支持,这进一步提高了其执行效率。

  2. 锁:相比之下,锁的使用通常会导致上下文切换,因为当一个线程试图获取已被其他线程持有的锁时,它会被阻塞并进入等待状态。这种上下文切换带来了额外的开销,并可能降低程序的总体性能。

三、资源占用与释放

  1. 原子操作:在执行过程中,原子操作会一直占用资源并等待条件满足。它不会被其他任务或事件中断,因此不需要释放资源。

  2. 锁:当一个线程获取锁并执行完临界区代码后,它会释放锁,从而允许其他线程获取锁并访问共享资源。这种机制确保了资源的合理利用和并发访问的安全性。

四、使用场景与灵活性

  1. 原子操作:原子操作通常用于简单的、短小的临界区代码,以及对性能要求极高的场景。由于原子操作的不可分割性,它非常适合于确保某些关键操作的原子性和一致性。

  2. 锁:锁提供了更高级别的并发控制,适用于更复杂的临界区代码和需要更精细控制并发访问的场景。锁还支持可重入性、可中断性和公平性等特性,为开发者提供了更多的灵活性和控制选项。

综上所述,原子操作和锁在定义、执行效率、资源占用与使用场景等方面存在显著差异。在选择使用哪种机制时,开发者应根据具体需求和场景进行权衡和选择。

 

posted @ 2024-06-16 23:42  DaiWK  阅读(17)  评论(0编辑  收藏  举报