c# 高级编程-多线程安全

本文探讨了在编程中如何判断线程安全,以及如何通过使用锁、单线程、线程安全集合类(如BlockingCollection、ConcurrentBag、ConcurrentDictionary、ConcurrentQueue和ConcurrentStack)和OrderablePartitioner来解决线程安全问题。着重介绍了.NET中提供的这些并发安全工具及其使用方法。
摘要由CSDN通过智能技术生成

概念:一段业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的;否则就表示是线程不安全;

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); // 生成一个包含11000的整数的数据集

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); // 生成一个包含11000的整数的数据集

Partitioner<int> partitioner = Partitioner.Create(data, loadBalance: true);

Parallel.ForEach(partitioner, (item, state) => {

    // 处理每个元素的逻辑

});

这里 loadBalance 参数表示是否启用负载均衡,它会尝试将工作均匀地分布到可用的线程中。

总体而言,Partitioner 提供了一种在并行计算中高效处理大规模数据集的方式。在许多情况下,你可以直接使用 Parallel.ForEach 或 Parallel.For,而不必显式创建 Partitioner 实例。

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值