概念:一段业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的;否则就表示是线程不安全;
var listInt = new List<int>();
for (int i = 0; i < 1000; i++)
{
Task.Run(() =>
{
listInt.Add(i);
});
}
每次执行出来的listInt的长度可能不一样,这就是线程不安全
如何解决线程安全问题:
1.锁,加锁--直接是标准锁---锁的本质,是独占引用;加锁以后;反多线程---不再是多线程执行了-可以解决线程安全问题---但是我不推荐大家使用--加锁--影响性能
List<int> intlist = new List<int>();
List<Task> tasklist = new List<Task>();
int k = 0;
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
lock (obj_Lock)
{
intlist.Add(i);
}
});
}
Task.WaitAll(tasklist.ToArray());
a) 标准写法: private readonly static object obj_Lock = new object();
b) 锁对象--不要去锁String 锁 This
2.直接使用单线程?(分块/分区去执行,每一个区块对应一个线程) 把要操作的整块数据,做一个切分:例如有1000条数据,可以把这一万条数据分区、分块;分三块;每一块开启一个线程去执行;三个线程。
每一个线程内部执行的动作是单线程--线程安全
等待三个线程执行结束以后,再单线程做一个统一汇总;
需要把程序加以设计才能完成;
// 分块,分区执行解决线程安全问题
var taskList = new List<Task>();
var listInt1 = new List<int>();
var listInt2 = new List<int>();
var listInt3 = new List<int>();
int length = 10000;
int n1 = 3000;
int n2 = 6000;
int n3 = 10000;
taskList.Add(Task.Run(() =>
{
for (int i = 0; i < n1; i++)
{
listInt1.Add(i);
}
}));
taskList.Add(Task.Run(() =>
{
for(int i = n1;i < n2; i++)
{
listInt2.Add(i);
}
}));
taskList.Add((Task)Task.Run(() =>
{
for (int i = n2; i < n3; i++)
{
listInt3.Add(i);
}
}));
Task.WaitAll(taskList.ToArray());
listInt1.AddRange(listInt2);
listInt1.AddRange(listInt3);
3.使用线程安全对象 看看数据结构 线程安全对象 List/Arraylist 都不是线程安全的集合--把list Arraylist 换成安全对象;
(1)BlockingCollection
// 创建 BlockingCollection<T> 实例:
BlockingCollection<int> ints = new BlockingCollection<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
ints.Add(i);// 在生产者线程中,将数据添加到集合中
});
}
补充:
BlockingCollection<T> 是 .NET 中提供的一个线程安全的集合类,用于在多线程环境中安全地传递和处理数据。它提供了一个简单而强大的机制,使得在生产者和消费者之间进行数据交换变得更加容易。
BlockingCollection<int> boundedCollection = new BlockingCollection<int>(capacity: 10); // 设置容量
当达到容量上限时,任何尝试向集合中添加元素的操作都将被阻塞,直到有消费者从集合中取走一些元素
BlockingCollection<T> 采用生产者-消费者模型,并且在内部使用了信号量和锁等机制,以确保多线程环境中的线程安全性。它提供了一种阻塞式的操作,当集合为空时,消费者会被阻塞,当集合达到容量上限时,生产者也会被阻塞。
此外,BlockingCollection<T> 还提供了其他一些方法,如TryAdd、TryTake等,以及可以传入 CancellationToken 进行取消操作的重载方法,使其更加灵活
(2)ConcurrentBag
ConcurrentBag<int> ints = new ConcurrentBag<int>();
for (int i = 0; i < 10000; i++)
{
Task.Run(() =>
{
ints.Add(i);
});
}
补充:
ConcurrentBag<T> 是 .NET 中提供的一个线程安全的集合类,用于在多线程环境中安全地存储和检索元素。与 BlockingCollection<T> 不同,ConcurrentBag<T> 不遵循生产者-消费者模型,而是允许多个线程同时进行添加和提取操作,因此它更适用于一些并发的场景。
基本用法
// 创建 ConcurrentBag<T> 实例:
ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>();
// 添加元素
// 在任意线程中
int data = GenerateData(); // 生成数据
concurrentBag.Add(data); // 将数据添加到集合中
// 提取元素
// 在任意线程中
if (concurrentBag.TryTake(out int data)) {
// 成功从集合中获取数据
} else {
// 集合为空
}
TryTake 方法是非阻塞的,如果集合为空,它会返回 false。
// 适用于多线程环境:
ConcurrentBag<T> 内部使用了一些并发安全的技术,允许多个线程同时进行添加和提取操作,而不需要显式的锁或同步机制。
// 不保证元素的顺序:
ConcurrentBag<T> 不保证元素的顺序,因此对于不要求顺序的场景,它是一个高效的选择。
总结:需要注意的是,ConcurrentBag<T> 不支持对集合进行修改的操作,例如删除指定元素等。它主要用于在多线程环境中安全地进行添加和提取操作。如果你需要支持更多的集合操作,可能需要考虑其他实现了 ICollection<T> 接口的线程安全集合,比如 ConcurrentQueue<T> 或 ConcurrentDictionary<TKey, TValue>。
总体而言,ConcurrentBag<T> 提供了一种在多线程环境中安全地处理元素的方式,特别适用于不要求特定顺序的场景。
(3)ConcurrentDictionary
ConcurrentDictionary<TKey, TValue> 是 .NET 中提供的一个线程安全的字典集合。它允许多个线程同时对字典进行读取和写入操作,而不需要显式的锁定。这使得它成为在多线程环境中安全地管理键值对数据的有力工具。
以下是 ConcurrentDictionary<TKey, TValue> 的基本用法:
// 创建 ConcurrentDictionary<TKey, TValue> 实例:
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
// 添加或更新元素:
// 在任意线程中
concurrentDictionary.TryAdd("Key1", 42); // 添加键值对
concurrentDictionary["Key2"] = 99; // 或者使用索引器语法直接赋值
// 获取元素
// 在任意线程中
if (concurrentDictionary.TryGetValue("Key1", out int value)) {
// 成功获取值
} else {
// 键不存在
}
// 移除元素
// 在任意线程中
concurrentDictionary.TryRemove("Key1", out int removedValue); // 移除键值对
// 遍历字典
// 在任意线程中
foreach (var kvp in concurrentDictionary) {
// 遍历字典中的键值对
}
// 原子性的更新操作
// 在任意线程中
concurrentDictionary.AddOrUpdate("Key1", 1, (key, existingValue) => existingValue + 1);
AddOrUpdate 方法允许你执行原子性的更新操作,即使在多线程环境中,也能保证逻辑的一致性。
总结:ConcurrentDictionary<TKey, TValue> 提供了在多线程环境中进行字典操作的简便方法,而无需显式的线程同步。它的性能在并发读取和写入方面较好,适用于高度并发的场景。
需要注意的是,虽然 ConcurrentDictionary<TKey, TValue> 对于大多数情况下的并发操作是足够的,但在某些特殊情况下,可能需要额外的同步或锁定。此外,如果需要按照特定顺序访问字典的键值对,可能需要考虑其他有序字典的实现。
(4)ConcurrentQueue
ConcurrentQueue<T> 是 .NET 中提供的一个线程安全的队列实现。它允许多个线程同时进行入队和出队操作,而不需要显式的锁定。这使得它成为在多线程环境中安全地进行先进先出 (FIFO) 操作的有力工具。
以下是 ConcurrentQueue<T> 的基本用法:
创建 ConcurrentQueue<T> 实例:
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
入队操作
// 在任意线程中
concurrentQueue.Enqueue(42); // 入队
出队操作
// 在任意线程中
if (concurrentQueue.TryDequeue(out int value)) {
// 成功出队
} else {
// 队列为空
}
TryDequeue 方法是非阻塞的,如果队列为空,它会返回 false。
查看队列头部元素而不移除:
// 在任意线程中
if (concurrentQueue.TryPeek(out int peekedValue)) {
// 成功查看队列头部元素
} else {
// 队列为空
}
判断队列是否为空:
// 在任意线程中
bool isEmpty = concurrentQueue.IsEmpty;
// 遍历队列
// 在任意线程中
foreach (var item in concurrentQueue) {
// 遍历队列中的元素
}
总结:ConcurrentQueue<T> 是基于锁-free技术实现的,并在内部使用了 CAS (Compare-and-Swap) 操作,以确保多线程环境下的线程安全。它适用于需要在多个线程之间共享数据,且希望通过队列的方式实现安全、高效的数据传递的场景。
需要注意的是,ConcurrentQueue<T> 的元素遵循先进先出 (FIFO) 的原则,因此保持了插入顺序。如果需要其他排序方式,可能需要考虑其他的并发集合,比如 ConcurrentBag<T> 或 ConcurrentDictionary<TKey, TValue>。
(5)ConcurrentStack
ConcurrentStack<T> 是 .NET 中提供的一个线程安全的栈实现。与 ConcurrentQueue<T> 类似,ConcurrentStack<T> 允许多个线程同时进行入栈和出栈操作,而不需要显式的锁定。这使得它成为在多线程环境中安全地进行后进先出 (LIFO) 操作的有力工具
以下是 ConcurrentStack<T> 的基本用法:
创建 ConcurrentStack<T> 实例
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
入栈操作:
// 在任意线程中
concurrentStack.Push(42); // 入栈
出栈操作:
// 在任意线程中
if (concurrentStack.TryPop(out int value)) {
// 成功出栈
} else {
// 栈为空
}
TryPop 方法是非阻塞的,如果栈为空,它会返回 false。
查看栈顶元素而不移除:
// 在任意线程中
if (concurrentStack.TryPeek(out int peekedValue)) {
// 成功查看栈顶元素
} else {
// 栈为空
}
判断栈是否为空:
// 在任意线程中
bool isEmpty = concurrentStack.IsEmpty;
遍历栈:
// 在任意线程中
foreach (var item in concurrentStack) {
// 遍历栈中的元素
}
ConcurrentStack<T> 是基于锁-free技术实现的,并在内部使用了 CAS (Compare-and-Swap) 操作,以确保多线程环境下的线程安全。它适用于需要在多个线程之间共享数据,且希望通过栈的方式实现安全、高效的数据传递的场景。
需要注意的是,ConcurrentStack<T> 的元素遵循后进先出 (LIFO) 的原则,因此保持了插入顺序。如果需要其他排序方式,可能需要考虑其他的并发集合,比如 ConcurrentQueue<T> 或 ConcurrentDictionary<TKey, TValue>
(6)OrderablePartitioner
OrderablePartitioner 是 .NET 中提供的一个用于支持并行迭代的抽象类。它允许将一个数据集分割成多个可并行处理的块,以便在并行计算中高效地处理大规模数据。
OrderablePartitioner 通常与 Parallel.ForEach 或 Parallel.For 配合使用,用于实现在迭代过程中的并行处理。与普通的 Partitioner 不同,OrderablePartitioner 能够保持迭代元素的顺序,确保处理的顺序与输入的顺序一致。
以下是 OrderablePartitioner 的基本用法:
创建 OrderablePartitioner 实例:
OrderablePartitioner<int> orderablePartitioner = new OrderablePartitioner<int>(GetSomeData());
GetSomeData() 是一个返回 IEnumerable<int> 的方法,它提供了要并行处理的数据集
使用 Parallel.ForEach 进行并行处理:
Parallel.ForEach(orderablePartitioner, (item, state) => {
// 处理每个元素的逻辑
});
在这个例子中,Parallel.ForEach 会根据 OrderablePartitioner 提供的分割策略,将数据集分割成多个块,然后并行地处理每个块。
OrderablePartitioner 的实现需要满足 IPartitioner<TSource> 接口,并提供 GetOrderableDynamicPartitions 和 GetOrderablePartitions 方法,用于获取动态和静态分区器。通常,你可以使用 Create 静态方法来创建一个适用于特定数据集的实例。
下面是一个简单的示例:
var data = Enumerable.Range(1, 1000); // 生成一个包含1到1000的整数的数据集
OrderablePartitioner<int> orderablePartitioner = Partitioner.Create(data, loadBalance: true);
Parallel.ForEach(orderablePartitioner, (item, state) => {
// 处理每个元素的逻辑
});
这里 loadBalance 参数表示是否启用负载均衡,它会尝试将工作均匀地分布到可用的线程中。
总体而言,OrderablePartitioner 提供了一种在并行处理中保持元素顺序的方式,适用于需要按顺序处理大规模数据集的情况。
(7)Partitioner
Partitioner 是 .NET 中提供的一个用于支持并行迭代的抽象类。它提供了一种将一个数据集分割成多个块的机制,以便在并行计算中高效地处理大规模数据。
Partitioner 类定义了两个主要方法:Create 和 Create<TSource>。这两个方法用于创建不同类型的分区器实例,以支持在并行计算中对数据的分割。通常,Partitioner 的实例会与 Parallel.ForEach 或 Parallel.For 配合使用。
以下是 Partitioner 的基本用法
创建 Partitioner 实例:
Partitioner<int> partitioner = Partitioner.Create(GetSomeData());
GetSomeData() 是一个返回 IEnumerable<int> 的方法,它提供了要并行处理的数据集。
使用 Parallel.ForEach 进行并行处理:
Parallel.ForEach(partitioner, (item, state) => {
// 处理每个元素的逻辑
});
在这个例子中,Parallel.ForEach 会根据 Partitioner 提供的分割策略,将数据集分割成多个块,然后并行地处理每个块。
Partitioner 的实现需要满足 IPartitioner<TSource> 接口,并提供 GetDynamicPartitions 和 GetPartitions 方法,用于获取动态和静态分区器。Partitioner 类的实现通常在内部使用一些启发式方法,以根据数据集的特性进行分割,以提高并行性能。
下面是一个简单的示例:
var data = Enumerable.Range(1, 1000); // 生成一个包含1到1000的整数的数据集
Partitioner<int> partitioner = Partitioner.Create(data, loadBalance: true);
Parallel.ForEach(partitioner, (item, state) => {
// 处理每个元素的逻辑
});
这里 loadBalance 参数表示是否启用负载均衡,它会尝试将工作均匀地分布到可用的线程中。
总体而言,Partitioner 提供了一种在并行计算中高效处理大规模数据集的方式。在许多情况下,你可以直接使用 Parallel.ForEach 或 Parallel.For,而不必显式创建 Partitioner 实例。