对于下面的一段代码(.NET v4.0.30319),我在第二个延续中得到了一个空引用异常。
最有趣的是,这个问题只发生在8GB内存的机器上,但是其他用户只有16GB甚至更多,他们没有报告任何问题,这是一个非常间歇性的问题,导致我怀疑垃圾收集问题。
可以多次调用GetData()
,因此_businessObjectTask的第一个延续只会被调用一次,因为_businessObjects将从那一点开始填充。
我想是因为
而引发Object reference not set to an instance of an object
异常
_businessObjectTask
为空,无法从空任务继续。 items
变量以某种方式为null 我的日志文件(748)中的行号指向下面突出显示的行号,而不是指向#1而不是#2的lambda表达式。我在Visual Studio中玩过,并且businessObjectTask.ContinueWith()
之后的每一行被认为是不同的,即如果它是lambda表达式中的空引用,它将给出不同的行号到748
非常感谢任何帮助。
编辑: 这与What is a NullReferenceException, and how do I fix it?无关,因为这是对空引用的更基本的解释,而这更加复杂和微妙。
异常
堆栈跟踪的完整详细信息(为简单起见编辑了虚拟类和命名空间名称)
Object reference not set to an instance of an object.
at ApplicationNamespace.ClassName`1.<>c__DisplayClass4e.<GetData>b__44(Task`1 t) in e:\ClassName.cs:line 748
at System.Threading.Tasks.ContinuationTaskFromResultTask`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
代码
private static IDictionary<string, IBusinessObject> _businessObjects;
private Task<IDictionary<string, IBusinessObject>> _businessObjectTask;
public Task GetData(IList<TBusinessItem> items))
{
Log.Info("Starting GetData()");
if (_businessObjects == null)
{
var businessObjectService = ServiceLocator.Current.GetInstance<IBusinessObjectService>();
_businessObjectTask = businessObjectService.GetData(CancellationToken.None)
.ContinueWith
(
t =>
{
_businessObjects = t.Result.ToDictionary(e => e.ItemId);
return _businessObjects;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Current
);
}
var taskSetLEData = _businessObjectTask.ContinueWith // Line 748 in my code - "Object reference not set to an instance of an object." thrown here
(
task =>
{
items.ToList().ForEach
(
item =>
{
IBusinessObject businessObject;
_businessObjects.TryGetValue(item.Id, out businessObject);
item.BusinessObject = businessObject;
}
);
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default
);
}
解决:
因此,在使用我从这个问题中学到的东西之后,我又回到了原始代码并想出了所有这些。
原来这个NRE的原因是因为_businessObjectTask
是非静态的,因为_businessObjects
是静态的。
这意味着_businessObjects
在第一次调用GetData()
时为空,然后将_businessObjectTask
设置为非空。然后当_businessObjectTask.ContinueWith
被调用时,它是非空的并且继续没有问题。
但是,如果实例化上述此类的第二个实例,则已填充_businessObjects
,因此_businessObjectTask
保持为空。然后,当调用_businessObjectTask.ContinueWith
时,会抛出_businessObjectTask
上的NRE。
我可以选择几个选项,但我最终将_businessObjectTask
删除为同步方法调用,这意味着我不再需要使用延续,并设置_businessObjects
一次。
答案 0 :(得分:1)
这是同步问题。
您假设_businessObjectTask
始终在_businessObjects
之前分配。
然而,这并不能保证。分配_businessObjects
的续约可能会在 businessObjectService.GetData(...).ContinueWith(...)
之前执行 inline ,因此会返回。
// This assignment could happend AFTER the inner assignment.
_businessObjectTask = businessObjectService.GetData(CancellationToken.None)
.ContinueWith
(
t =>
{
// This assignment could happen BEFORE the outer assignment.
_businessObjects = t.Result.ToDictionary(e => e.ItemId);
因此,虽然_businessObjects
为空,但_businessObjectTask
可能不为空。
如果并发线程此时会输入您的GetData
方法,则显然不会输入
if (_businessObjects == null) // not entered because it's not null
{
...
}
...而是继续
var taskSetLEData = _businessObjectTask.ContinueWith // line 748
...这将导致空引用异常,因为_businessObjectTask
为空。
以下是如何简化代码并解决此同步问题的方法:
private Lazy<Task<IDictionary<string, IBusinessObject>>> _lazyTask =
new Lazy<Task<IDictionary<string, IBusinessObject>>>(FetchBusinessObjects);
private static async Task<IDictionary<string, IBusinessObject>> FetchBusinessObjects()
{
var businessObjectService = ServiceLocator.Current.GetInstance<IBusinessObjectService>();
return await businessObjectService.GetData(CancellationToken.None).ToDictionary(e => e.ItemId);
}
public async Task GetData(IList<TBusinessItem> items)
{
Log.Info("Starting GetData()");
var businessObjects = await _lazyTask.Value;
items.ToList().ForEach
(
item =>
{
IBusinessObject businessObject;
businessObjects.TryGetValue(item.Id, out businessObject);
item.BusinessObject = businessObject;
}
);
}
注意:
使用Lazy<T>
确保业务对象服务仅被调用一次(每个类的实例,无论它是什么)。
使用async
/ await
来简化代码。
您可能需要考虑将_lazyTask
声明为静态。在您的代码中,静态/非静态字段之间似乎存在混淆。我不知道哪个适合你。