我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C# 线程安全详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C# 线程安全详解

介绍

在 .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 等等,都很容易理解。

总结

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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C# 线程安全详解

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

下载Word文档

猜你喜欢

【Linux】线程安全(万字详解)

🎇Linux: 博客主页:一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义
2023-08-19

C#多线程安全怎么理解

这篇文章主要介绍“C#多线程安全怎么理解”,在日常操作中,相信很多人在C#多线程安全怎么理解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C#多线程安全怎么理解”的疑惑有所帮助!接下来,请跟着小编一起来学习吧
2023-06-22

Java线程安全与非线程安全解析

ArrayList和Vector有什么区别?HashMap和HashTable有什么区别?StringBuilder和StringBuffer有什么区别?这些都是Java面试中常见的基础问题。面对这样的问题,回答是:ArrayList是非线
2023-05-31

【Java系列】详解多线程(三)—— 线程安全(下篇)

个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得,欢迎大家在评
【Java系列】详解多线程(三)—— 线程安全(下篇)
2023-12-22

SimpleDateFormat线程安全问题排查详解

这篇文章主要为大家介绍了SimpleDateFormat线程安全问题排查详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-11-13

Java多线程 - 线程安全和线程同步解决线程安全问题

文章目录 线程安全问题线程同步方式一: 同步代码块方式二: 同步方法方式三: Lock锁 线程安全问题 线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 举例:
2023-08-20

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录