Конкретная причина получения NullReferenceException
заключается в том, что внутреннее состояние контейнера Dictionary
повреждено. Вероятно, два потока пытались изменить размер двух внутренних массивов Dictionary
параллельно, или что-то еще одинаково неприятное. На самом деле вам повезло, что вы получили эти исключения, потому что гораздо худшим результатом будет наличие работающей программы, которая дает неверные результаты.
Более общая причина этой проблемы заключается в том, что вы разрешили параллельный асинхронизированный доступ к потоку. небезопасные объекты. Класс Dictionary
, как и большинство встроенных классов .NET, не является поточно-ориентированным. Это реализовано в предположении, что к нему будет обращаться один поток (или хотя бы один поток за раз). Он не содержит внутренней синхронизации. Причина заключается в том, что добавление синхронизации в класс влечет за собой сложность API и снижение производительности, и нет никаких причин платить эти издержки каждый раз, когда вы используете этот класс, когда это потребуется только в нескольких особых случаях.
Есть много решений вашей проблемы. Один из них - продолжать использовать небезопасный поток Dictionary
, но обеспечить доступ к нему исключительно с помощью блокировок. Это наиболее гибкое решение, но вы должны быть очень осторожны, чтобы не допустить ни одного незащищенного пути кода к объекту. Доступ к каждому свойству и каждому методу с чтением или записи в него должен быть внутри lock
. Так что это гибко, но хрупко, и может стать узким местом производительности в случае сильной конкуренции (т. Е. Слишком много потоков одновременно запрашивают эксклюзивную блокировку и вынуждены ждать в очереди).
Другое решение состоит в том, чтобыиспользуйте потокобезопасный контейнер, такой как ConcurrentDictionary
. Этот класс гарантирует, что его внутреннее состояние никогда не будет повреждено при параллельном доступе нескольких потоков. К сожалению, это не гарантирует ничего из остального состояния вашей программы. Так что это подходит для некоторых простых случаев, когда у вас нет другого общего состояния, кроме как из самого словаря. В этих случаях он предлагает повышение производительности, поскольку он реализован с гранулярной внутренней блокировкой (имеется несколько блокировок, по одной на каждый сегмент данных).
Лучшее решение - полностью устранить необходимость синхронизации потоков, устраняя необходимостьобщее состояние. Просто дайте каждому потоку работать со своим внутренним изолированным подмножеством или данными, и объединяйте эти подмножества только после завершения всех потоков. Обычно это обеспечивает наилучшую производительность за счет необходимости разбить начальную рабочую нагрузку и затем написать окончательный код слияния. Есть библиотеки, которые следуют этой стратегии, но имеют дело со всем этим образцом, позволяя вам писать как можно меньше кода. Одна из лучших - это библиотека TPL Dataflow , которая фактически встроена в платформу .NET Core. Для .NET Framework вам необходимо установить пакет, чтобы использовать его.