C# 线程安全详解
作者:菜鸟厚非 发布时间:2023-02-07 10:40:46
介绍
在 .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 等等,都很容易理解。
来源:https://blog.csdn.net/weixin_46785144/article/details/120316962


猜你喜欢
- 简述增量更新,根据字面理解,就是下载增加的那部分来达到更新的目的,实际就是这个意思。原理用一个旧的Apk安装与一个新的Apk安装包使用 bs
- 今天没事跟群里面侃大山,有个哥们说道Android Wheel这个控件,以为是Andriod内置的控件,google一把,发现是个githu
- 1、Aware 系列接口Aware 系列接口是用来获取 Spring 内部对象的接口。Aware 自身是一个顶级接口,它有一系列子接口,在一
- Activity回顾activity是android程序中最重要的组件之一,它是用户与android用户交互的主要组件,类似于桌面程序的图形
- 一直想在持续集成方向学习并研究一番,近期正准备结合jmeter+ant+jenkins做自动化接口测试,在学习的同时,正好实践一番,毕竟实践
- 本文实例讲述了android通过Location API显示地址信息的实现方法。分享给大家供大家参考。具体如下:android的Locati
- 这篇文章主要介绍了SpringCloud断路器Hystrix原理及用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参
- 话不多说,请看代码/// <summary>/// 删除字符串中的中文/// </summary>public st
- A:首先先看下一个简单的面试题斐波那契数列计算数组{1,1,2,3,5,8.......} 第30位值规律:1 1 从第三项开始,每一项都是
- JAVA基础八股文Switch能支持哪些类型?jdk5之前,switch能够作用在byte,short,char,int(实际上都是提升为i
- 在研究性能的时候,完全可以使用Stopwatch计时器计算一项技术的效率。但是有时想知道某想技术的性能的时候,又常常想不起可以运用Stopw
- 许久不来 , 冒个泡 , 发一个刚做的声音波动的View吧 : 代码不多 , 没什么技术含量 , 权当给您省时间了 , 直接复制粘贴就能用
- 本文实例为大家分享了C#实现银行家算法的具体代码,供大家参考,具体内容如下1.死锁死锁,顾名思义,是一种锁住不可自行解开的死局。在操作系统中
- 首先,我们看看Map架构。如上图:Map 是映射接口,Map中存储的内容是键值对(key-value)。AbstractMap 是继承于Ma
- FileOutPutStream:子类,写出数据的通道步骤:1.获取目标文件2.创建通道(如果原来没有目标文件,则会自动创建一个)3.写入数
- 使用IDEA配置Maven搭建开发框架ssm教程一、配置Maven环境1.下载Maven:下载链接2.下载完成解压压缩包并创建本地仓库文件夹
- 错误处理到目前为止,我们都没怎么介绍onComplete()和onError()函数。这两个函数用来通知订阅者,被观察的对象将停止发送数据以
- 本文实例讲述了Android中Market的Loading效果实现方法。分享给大家供大家参考。具体如下:在Android中,要实现Loadi
- CountDownTimerCountDownTimer 是android 自带的一个倒计时类,使用这个类可以很简单的实现 倒计时功能Cou
- 本文实例为大家分享了C# DateTime预设可选的日期范围的相关代码,可以选择本年度、本季度、本月等,供大家参考,具体内容如下效果:大家在