iis服务器助手广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C# 线程安全详解
  • 654
分享到

C# 线程安全详解

2024-04-02 19:04:59 654人浏览 八月长安
摘要

目录介绍经典生产消费问题介绍QueueConcurrentQueueBlockinGCollectionBlockingCollection 枚举BlockingCollection

介绍

.net4.0 之前,如果我们需要在多线程环境下使用 Dictionary 类,除了自己实现线程同步来保证线程安全外,我们没有其他选择。很多开发人员肯定都实现过类似的线程安全方案,可能是通过创建全新的线程安全字典,或者仅是简单的用一个类封装一个 Dictionary 对象,并在所有方法中加上机制,我们称这种方案叫 “Dictionary+Locks” 。

System.Collections.Concurrent 命名空间下提供多个线程安全集合类,只要多个线程同时访问集合,就应使用这些类来代替 System.Collections 和 System.Collections.Generic 命名空间中的相应类型。 但是,不保证通过扩展方法或通过显式接口实现访问集合对象是线程安全的,可能需要由调用方进行同步。

经典生产消费问题

介绍

这个问题是最为经典的多线程应用问题问题就是:有一个或多个线程(生产者线程)产生一些数据,还有一个或者多个线程(消费者线程)要取出这些数据并执行一些相应的工作。

在这里插入图片描述

Queue

接下来,我们是使用程序去描述这个问题,看下面代码


static void Main(string[] args)
{
    int count = 0;
    // 临界资源区
    var queue = new Queue<string>();
    // 生产者线程
    Task.Factory.StartNew(() =>
    {
        while (true)
        {
            queue.Enqueue("mesg" + count);
            count++;
        }
    });
    // 消费者线程1
    Task.Factory.StartNew(() =>
    {
        while (true)
        {
            if (queue.Count > 0)
            {
                string value = queue.Dequeue();
                Console.WriteLine("Worker A: " + value);
            }
        }
    });
    // 消费者线程2
    Task.Factory.StartNew(() =>
    {
        while (true)
        {
            if (queue.Count > 0)
            {
                string value = queue.Dequeue();
                Console.WriteLine("Worker B: " + value);
            }
        }
    });
    Thread.Sleep(50000);
}

我们使用 Queue 模拟了一个简单的资源池,一个生产者放数据,两个消费者消费数据。

这个程序运行以后会产生异常,异常的原因很简单。当某时刻,第一个消费者判断 queue.Count > 0 为true 时,就会到 Queue 中取数据。但是,此时这个数据可能会被第二个消费者拿走了,因为第二个消费者也判断出此时有数据可取。第一个消费者取取数据时就会发生异常,这就是一个简单的临界资源线程安全问题。

知道问题了,那么如何解决呢?有两种方案,接下来进行讲解

ConcurrentQueue

1 . 加锁

这个方案是可行的,很多时候我们也是这么做的,包括微软早期实现线程安全的 ArrayList 和 Hashtable 内部 (Synchronized方法) 也是这么实现的。这个方案适用于只有少量的消费者,并且每个消费者都会执行大量操作的时候,这时 lock 并没什么太大问题,但是,如果是大批量短小精悍的消费者存在的话,lock 会严重影响代码的执行效率。

2 . 线程安全的集合区

这个就是 .NET4.0 后 System.Collections.Concurrent 命名空间下提供多个线程安全集合类方案。

新的线程安全的这些集合内部不再使用lock机制这种比较低效的方式去实现线程安全,而是转而使用SpinWait 和 Interlocked 等机制,间接实现了线程安全,这种方式的效率要高于使用lock的方式。


var queue = new ConcurrentQueue<string>();
Task.Factory.StartNew(() =>
{
    while (true)
    {
        queue.Enqueue("msg" + count);
        count++;
    }
});
Task.Factory.StartNew(() =>
{
    while (true)
    {
        string value;
        if (queue.TryDequeue(out value))
        {
            Console.WriteLine("Worker A: " + value);
        }
    }
});
Task.Factory.StartNew(() =>
{
    while (true)
    {
        string value;
        if (queue.TryDequeue(out value))
        {
            Console.WriteLine("Worker B: " + value);
        }
    }
});

ConcurrentQueue.TryDequeue(T) 方法会尝试获取消费,那能不能不要去判断集合是否为空,集合当自己没有元素的时候自己 Block 一下可以吗?答案是,可以的

BlockingCollection

针对上面的问题,我们可以使用 BlockingCollection 即可。接下来我来看


var blockingCollection = new BlockingCollection<string>();
Task.Factory.StartNew(() =>
{
    while (true)
    {
        blockingCollection.Add("msg" + count);
        count++;
    }
});
Task.Factory.StartNew(() =>
{
    while (true)
    {
        Console.WriteLine("Worker A: " + blockingCollection.Take());
    }
});
Task.Factory.StartNew(() =>
{
    while (true)
    {
        Console.WriteLine("Worker B: " + blockingCollection.Take());
    }
});

BlockingCollection 集合是一个拥有阻塞功能的集合,它就是完成了经典生产者消费者的算法功能。它没有实现底层的存储结构,而是使用了实现 IProducerConsumerCollection 接口的几个集合作为底层的数据结构,例如 ConcurrentBag, ConcurrentStack 或者是 ConcurrentQueue。你可以在构造BlockingCollection 实例的时候传入这个参数,如果不指定的话,则默认使用 ConcurrentQueue 作为存储结构。

而对于生产者来说,只需要通过调用其Add方法放数据,消费者只需要调用Take方法来取数据就可以了。

当然了上面的消费者代码中还有一点是让人不爽的,那就是 while 语句,可以更优雅一点吗?答案是,可以的。


Task.Factory.StartNew(() =>
{
        foreach (string value in blockingCollection.GetConsumingEnumerable())
        {
            Console.WriteLine("Worker A: " + value);
        }
});

BlockingCollection.GetConsumingEnumerable 方法是关键,这个方法会遍历集合取出数据,一旦发现集合空了,则阻塞自己,直到集合中又有元素了再开始遍历。

此时,完美了解决了生产者消费者问题。然而通常来说,还有下面两个问题我们有时需要去控制

1 . 控制集合中数据的最大数量

这个问题由 BlockingCollection 构造函数解决,构造该对象实例的时候,构造函数中的 BoundedCapacity 决定了集合最大的可容纳数据数量,这个比较简单。

2 . 何时停止的问题

这个问题由 CompleteAdding 和 IsCompleted 两个配合解决。CompleteAdding 方法是直接不允许任何元素被加入集合;当使用了 CompleteAdding 方法后且集合内没有元素的时候,另一个属性 IsCompleted 此时会为 True,这个属性可以用来判断是否当前集合内的所有元素都被处理完。生产者修改后的代码:


Task.Factory.StartNew(() =>
{
    for (int count = 0; count < 10; count++)
    {
        blockingCollection.Add("msg" + count);
    }
    blockingCollection.CompleteAdding();
});

当使用了 CompleteAdding 方法后,对象停止往集合中添加数据,这时如果是使用 GetConsumingEnumerable 枚举的,那么这种枚举会自然结束,不会再 Block 住集合,这种方式最优雅,也是推荐的写法。

但是如果是使用 TryTake 访问元素的,则需要使用 IsCompleted 判断一下,因为这个时候使用 TryTake 会抛InvalidOperationException 异常。接着我们看下最后的完整代码:


static void Main(string[] args)
{
    var blockingCollection = new BlockingCollection<string>();
    var producer = Task.Factory.StartNew(() =>
    {
        for (int count = 0; count < 10; count++)
        {
            blockingCollection.Add("msg" + count);
            Thread.Sleep(300);
        }
        blockingCollection.CompleteAdding();
    });
    var consumer1 = Task.Factory.StartNew(() =>
    {
        foreach (string value in blockingCollection.GetConsumingEnumerable())
        {
            Console.WriteLine("Worker A: " + value);
        }
    });
    var consumer2 = Task.Factory.StartNew(() =>
    {
        foreach (string value in blockingCollection.GetConsumingEnumerable())
        {
            Console.WriteLine("Worker B: " + value);
        }
    });
    Task.WaitAll(producer, consumer1, consumer2);
}

BlockingCollection 枚举

此外,需要注意 BlockingCollection 有两种枚举方法,

1 . foreach

首先 BlockingCollection 本身继承自IEnumerable,所以它自己就可以被 foreach 枚举,首先 BlockingCollection 包装了一个线程安全集合,那么它自己也是线程安全的,而当多个线程在同时修改或访问线程安全容器时,BlockingCollection 自己作为 IEnumerable 会返回一个一定时间内的集合片段,也就是只会枚举在那个时间点上内部集合的元素。使用这种方式枚举的时候,不会有 Block 效果。

2 . GetConsumingEnumerable

另外一种方式就是我们上面使用的 GetConsumingEnumerable 方式的枚举,这种方式会有 Block 效果,直到 CompleteAdding 被调用为止。

BlockingCollection 扩展

实现 IProducerConsumerCollection 接口的几个集合:ConcurrentBag (线程安全的无序的元素集合), ConcurrentStack (线程安全的堆栈) 和 ConcurrentQueue (线程安全的队列)。这些都很简单,功能与非线程安全的那些集合都一样,只不过是多了 TryXXX 方法,多线程环境下使用这些方法就好了。

System.Collections.Concurrent

System.Collections.Concurrent 下面还有一些其他与多线程相关的集合,有些个类在原来的基础上也添加了一下新的方法,例如:AddOrUpdate,GetOrAdd,TryXXX 等等,都很容易理解。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!

--结束END--

本文标题: C# 线程安全详解

本文链接: https://www.lsjlt.com/news/136107.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C# 线程安全详解
    目录介绍经典生产消费问题介绍QueueConcurrentQueueBlockingCollectionBlockingCollection 枚举BlockingCollection...
    99+
    2024-04-02
  • 【Linux】线程安全(万字详解)
    🎇Linux: 博客主页:一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义...
    99+
    2023-08-19
    linux 运维 服务器
  • Java多线程之线程安全问题详解
    目录1. 什么是线程安全和线程不安全?2. 自增运算为什么不是线程安全的?3. 临界区资源和竞态条件总结:面试题: 什么是线程安全和线程不安全?自增运算是不是线程安全的?如何保证多线...
    99+
    2024-04-02
  • java多线程创建及线程安全详解
    什么是线程 线程被称为轻量级进程,是程序执行的最小单位,它是指在程序执行过程中,能够执行代码的一个执行单位。每个程序程序都至少有一个线程,也即是程序本身。 线程...
    99+
    2024-04-02
  • C#多线程安全怎么理解
    这篇文章主要介绍“C#多线程安全怎么理解”,在日常操作中,相信很多人在C#多线程安全怎么理解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C#多线程安全怎么理解”的疑惑有所帮助!接下来,请跟着小编一起来学习吧...
    99+
    2023-06-22
  • 深入了解C#多线程安全
    目录什么是多线程安全?多线程安全示例1. 多线程不安全示例12. 多线程不安全示例2加锁lock加锁原理为何锁对象要用私有类型?为什么锁对象要用static类型?加锁锁定的是什么?泛...
    99+
    2024-04-02
  • Java线程安全与非线程安全解析
    ArrayList和Vector有什么区别?HashMap和HashTable有什么区别?StringBuilder和StringBuffer有什么区别?这些都是Java面试中常见的基础问题。面对这样的问题,回答是:ArrayList是非线...
    99+
    2023-05-31
    java 线程安全 ava
  • SimpleDateFormat线程安全问题排查详解
    目录一. 问题现象二. 原因排查三. 原因分析四. 解决方案一. 问题现象 运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面。 二. 原因排查 首先,检查...
    99+
    2022-11-13
    SimpleDateFormat线程安全排查 SimpleDateFormat线程安全
  • 【Java系列】详解多线程(三)—— 线程安全(下篇)
    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得,欢迎大家在评...
    99+
    2023-12-22
    java 安全 多线程 java-ee
  • Java使用线程同步解决线程安全问题详解
    第一种方法:同步代码块: 作用:把出现线程安全的核心代码上锁 原理:每次只能一个线程进入,执行完毕后自行解锁,其他线程才能进来执行 锁对象要求:理论上,锁对象只要对于当前同时执行的线...
    99+
    2024-04-02
  • Java多线程 - 线程安全和线程同步解决线程安全问题
    文章目录 线程安全问题线程同步方式一: 同步代码块方式二: 同步方法方式三: Lock锁 线程安全问题 线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 举例:...
    99+
    2023-08-20
    java 安全 jvm
  • C++线程安全的队列你了解嘛
    目录无界队列有界队列总结 无界队列 #include<queue> #include<mutex> #include<condition_variabl...
    99+
    2024-04-02
  • 详解CopyOnWriteArrayList是如何保证线程安全
    目录一:前言二:成员变量分析三:源码分析1.空参构造2.传入一个Collection对象的构造方法3.传入一个数组的构造方法四:总结一:前言 在我们需要保证线程安全的时候,如果使用到...
    99+
    2024-04-02
  • 关于java中线程安全问题详解
    目录一、什么时候数据在多线程并发的环境下会存在安全问题?二、怎么解决线程安全问题?三、银行 取钱/存钱 案例为什么会出现线程安全问题四、总结 一、什么时候数据在多线程并发的环境下会存...
    99+
    2024-04-02
  • Java多线程之线程安全问题详情
    目录1.线程安全概述1.1什么是线程安全问题1.2一个存在线程安全问题的程序2.线程加锁与线程不安全的原因2.1案例分析2.2线程加锁2.2.1什么是加锁2.2.2如何加锁2.2.3...
    99+
    2024-04-02
  • C++多线程编程详解
    目录C++多线程1. 概念1.1 概念2. 常用API1.thread2.互斥锁mutex3. 挂起和唤醒3. 应用场景3.1 call_once执行一次的函数3.2 conditi...
    99+
    2024-04-02
  • Spring在SingleTon模式下的线程安全详解
    目录1、有状态的bean与无状态的bean2、Spring中的单例3、Spring使用ThreadLocal解决线程安全问题案例4、ThreadLocal与线程同步机制的比较1、有状...
    99+
    2024-04-02
  • C++线程之thread详解
    目录1.创建线程2.守护线程3.可调用对象4.传参5.线程的移动和复制6.线程id7.互斥mutex总结1.创建线程 直接初始话thread类对象进行创建线程,创建线程后调用join...
    99+
    2024-04-02
  • Java中线程状态+线程安全问题+synchronized的用法详解
    目录java中的线程状态线程安全问题案例分析多线程对同一变量进行写操作内存可见性问题指令重排序问题synchronized的用法synchronized起作用的本质修饰普通方法修饰静...
    99+
    2024-04-02
  • 一文详解Java线程中的安全策略
    目录一、不可变对象二、线程封闭三、线程不安全类与写法四、线程安全-同步容器1. ArrayList -> Vector, Stack2. HashMap -> HashT...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作