通过使用任务提高处理速度

问题描述 投票:2回答:3

我有以下代码:

class Program
{
    class ProcessedEven
    {
        public int ProcessedInt { get; set; }

        public DateTime ProcessedValue { get; set; }
    }

    class ProcessedOdd
    {
        public int ProcessedInt { get; set; }

        public string ProcessedValue { get; set; }
    }

    static void Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();

        IEnumerator<int> enumerator = Enumerable.Range(0, 100000).GetEnumerator();
        Dictionary<int, ProcessedOdd> processedOddValuesDictionary = new Dictionary<int, ProcessedOdd>();
        Dictionary<int, ProcessedEven> processedEvenValuesDictionary = new Dictionary<int, ProcessedEven>();

        stopwatch.Start();

        while (enumerator.MoveNext())
        {
            int currentNumber = enumerator.Current;

            if (currentNumber % 2 == 0)
            {
                Task.Run(() =>
                {
                    ProcessedEven processedEven =
                        new ProcessedEven { ProcessedInt = currentNumber, ProcessedValue = DateTime.Now.AddMinutes(currentNumber) };
                    await Task.Delay(100);

                    processedEvenValuesDictionary.Add(currentNumber, processedEven);
                });
            }
            else
            {
                Task.Run(() =>
                {
                    ProcessedOdd processedOdd =
                        new ProcessedOdd { ProcessedInt = currentNumber, ProcessedValue = Math.Pow(currentNumber, 4).ToString() };
                    await Task.Delay(100);

                    processedOddValuesDictionary.Add(currentNumber, processedOdd);
                });
            }
        }

        stopwatch.Stop();

        Console.WriteLine(stopwatch.Elapsed.TotalSeconds);

        Console.ReadKey();
    }

所以基本上,我必须遍历一直同步的枚举器。

一旦获得了迭代器的当前值,它就会以某种方式花费很长时间来处理。 After根据其值被添加到字典中进行处理。因此,最后必须用正确的值填充字典。

为了提高速度,我认为引入一些并行性可能会有所帮助,但是在添加“ Task.Run”之后会调用一些

“ System.NullReferenceException:'对象引用未设置为对象的实例”

发生异常。与该代码的“同步”版本相比,执行时间也增加了(没有“ Task.Run”调用的代码)。

我不明白为什么会引发这些异常,因为一切似乎都不为空。

在这种情况下是否可以通过使用多线程来提高速度(原始代码没有“ Task.Run”调用?

由于字典似乎是在任务之间共享的,因此应该在lock语句中将已处理元素添加到字典中吗?

c# .net multithreading task-parallel-library
3个回答
2
投票

您正在创建许多小任务,并通过调用Task.Run耗尽线程池。最好使用Parallel.ForEach以获得更好的性能。正如@ user1672994所说,您应该使用线程安全版本的Parallel.ForEach-Dictionary

ConcurrentDictionary

我也不明白为什么您的代码中需要static void Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); IEnumerable<int> enumerable = Enumerable.Range(0, 100000); ConcurrentDictionary<int, ProcessedOdd> processedOddValuesDictionary = new ConcurrentDictionary<int, ProcessedOdd>(); ConcurrentDictionary<int, ProcessedEven> processedEvenValuesDictionary = new ConcurrentDictionary<int, ProcessedEven>(); stopwatch.Start(); Parallel.ForEach(enumerable, currentNumber => { if (currentNumber % 2 == 0) { ProcessedEven processedEven = new ProcessedEven { ProcessedInt = currentNumber, ProcessedValue = DateTime.Now.AddMinutes(currentNumber) }; // Task.Delay(100); processedEvenValuesDictionary.TryAdd(currentNumber, processedEven); } else { ProcessedOdd processedOdd = new ProcessedOdd { ProcessedInt = currentNumber, ProcessedValue = Math.Pow(currentNumber, 4).ToString() }; // Task.Delay(100); processedOddValuesDictionary.TryAdd(currentNumber, processedOdd); } }); stopwatch.Stop(); Console.WriteLine(stopwatch.Elapsed.TotalSeconds); Console.ReadKey(); } 。无论如何,如果没有Task.Delay(100)运算符,它将是您可能不会期望的事情,这是异步操作。醚使用等待或使用同步版本await


1
投票

您应使用Thread.Sleep(100),它是键/值对的线程安全集合,可以同时由多个线程访问。

ConcurrentDictionary设计用于多线程方案。您不必在代码中使用锁即可添加或删除集合中的项目。但是,一个线程总是有可能检索值,而另一个线程总是可以通过给同一个键一个新值来立即更新集合。

当我在将ConcurrentDictionary更改为ConcurrentDictionary后运行您的代码时,代码将在没有Dictionary的情况下运行并在约1.37秒内完成。

完整代码:

ConcurrentDictionary

NullReferenceException


1
投票

获得 class Program { class ProcessedEven { public int ProcessedInt { get; set; } public DateTime ProcessedValue { get; set; } } class ProcessedOdd { public int ProcessedInt { get; set; } public string ProcessedValue { get; set; } } static void Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); IEnumerator<int> enumerator = Enumerable.Range(0, 100000).GetEnumerator(); ConcurrentDictionary<int, ProcessedOdd> processedOddValuesDictionary = new ConcurrentDictionary<int, ProcessedOdd>(); ConcurrentDictionary<int, ProcessedEven> processedEvenValuesDictionary = new ConcurrentDictionary<int, ProcessedEven>(); stopwatch.Start(); while (enumerator.MoveNext()) { int currentNumber = enumerator.Current; if (currentNumber % 2 == 0) { Task.Run(() => { ProcessedEven processedEven = new ProcessedEven { ProcessedInt = currentNumber, ProcessedValue = DateTime.Now.AddMinutes(currentNumber) }; Task.Delay(100); processedEvenValuesDictionary.TryAdd(currentNumber, processedEven); }); } else { Task.Run(() => { ProcessedOdd processedOdd = new ProcessedOdd { ProcessedInt = currentNumber, ProcessedValue = Math.Pow(currentNumber, 4).ToString() }; Task.Delay(100); processedOddValuesDictionary.TryAdd(currentNumber, processedOdd); }); } } stopwatch.Stop(); Console.WriteLine(stopwatch.Elapsed.TotalSeconds); Console.ReadKey(); } } 的特定原因是因为enter image description here容器的内部状态已损坏。可能有两个线程试图并行调整NullReferenceException的两个内部数组的大小,或者同样令人讨厌。实际上,您很幸运能收到这些例外,因为更糟糕的结果是要有一个工作程序才能产生不正确的结果。

此问题的更一般的原因是,您允许并行异步访问线程不安全的对象。与大多数内置的.NET类一样,Dictionary类也不是线程安全的。它的实现假设是将由一个线程(或一次至少一个线程)访问。它不包含内部同步。原因是在类中添加同步会增加API的复杂性和性能开销,并且仅在少数特殊情况下才需要使用此类,因此没有理由每次使用此类开销。

您有很多解决方案。一种是继续使用线程不安全Dictionary,但要确保将通过使用锁以独占方式对其进行访问。这是最灵活的解决方案,但是您需要非常小心,甚至不允许单个不受保护的代码路径到达对象。访问every

属性和every方法(要么读取or对其进行写操作)必须位于Dictionary内部。因此,这是灵活的但易碎的,并且可能在争用过多的情况下成为性能瓶颈(即,太多线程同时请求独占锁,并被迫排队等待)。

另一个解决方案是使用线程安全容器,例如Dictionary。此类确保当并行由多个线程访问时,其内部状态永远不会损坏。不幸的是,它无法确保程序其余部分的状态。因此,对于某些简单情况,除了字典本身之外,您没有其他共享状态非常适合。在这种情况下,由于它使用精细的内部锁定(存在多个锁定,每个数据段一个),因此可以提高性能。

最佳解决方案是通过消除共享状态来完全消除线程同步的需要。只需让每个线程使用其内部隔离的子集或数据,并且仅在完成所有线程后才合并这些子集。这通常可以提供最佳性能,但必须先划分初始工作负载,然后编写最终合并代码。有一些库遵循此策略,但是正在处理所有这些样板文件,从而使您可以编写尽可能少的代码。最好的之一是lock,它实际上是嵌入在.NET Core平台中的。对于.NET Framework,您需要安装一个软件包才能使用它。

© www.soinside.com 2019 - 2024. All rights reserved.