我有以下代码:
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语句中将已处理元素添加到字典中吗?
您正在创建许多小任务,并通过调用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
您应使用Thread.Sleep(100)
,它是键/值对的线程安全集合,可以同时由多个线程访问。
ConcurrentDictionary
设计用于多线程方案。您不必在代码中使用锁即可添加或删除集合中的项目。但是,一个线程总是有可能检索值,而另一个线程总是可以通过给同一个键一个新值来立即更新集合。
当我在将ConcurrentDictionary
更改为ConcurrentDictionary
后运行您的代码时,代码将在没有Dictionary
的情况下运行并在约1.37秒内完成。
完整代码:
ConcurrentDictionary
NullReferenceException
获得 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();
}
}
的特定原因是因为容器的内部状态已损坏。可能有两个线程试图并行调整NullReferenceException
的两个内部数组的大小,或者同样令人讨厌。实际上,您很幸运能收到这些例外,因为更糟糕的结果是要有一个工作程序才能产生不正确的结果。
此问题的更一般的原因是,您允许并行异步访问线程不安全的对象。与大多数内置的.NET类一样,Dictionary
类也不是线程安全的。它的实现假设是将由一个线程(或一次至少一个线程)访问。它不包含内部同步。原因是在类中添加同步会增加API的复杂性和性能开销,并且仅在少数特殊情况下才需要使用此类,因此没有理由每次使用此类开销。
您有很多解决方案。一种是继续使用线程不安全Dictionary
,但要确保将通过使用锁以独占方式对其进行访问。这是最灵活的解决方案,但是您需要非常小心,甚至不允许单个不受保护的代码路径到达对象。访问every
Dictionary
内部。因此,这是灵活的但易碎的,并且可能在争用过多的情况下成为性能瓶颈(即,太多线程同时请求独占锁,并被迫排队等待)。另一个解决方案是使用线程安全容器,例如Dictionary
。此类确保当并行由多个线程访问时,其内部状态永远不会损坏。不幸的是,它无法确保程序其余部分的状态。因此,对于某些简单情况,除了字典本身之外,您没有其他共享状态非常适合。在这种情况下,由于它使用精细的内部锁定(存在多个锁定,每个数据段一个),因此可以提高性能。
最佳解决方案是通过消除共享状态来完全消除线程同步的需要。只需让每个线程使用其内部隔离的子集或数据,并且仅在完成所有线程后才合并这些子集。这通常可以提供最佳性能,但必须先划分初始工作负载,然后编写最终合并代码。有一些库遵循此策略,但是正在处理所有这些样板文件,从而使您可以编写尽可能少的代码。最好的之一是lock
,它实际上是嵌入在.NET Core平台中的。对于.NET Framework,您需要安装一个软件包才能使用它。