空引用-任务ContinueWith()
本文关键字:ContinueWith 任务 引用 | 更新日期: 2023-09-27 18:26:19
对于下面的代码(.NET v4.0.30119),我在第二个续文中得到了一个null引用异常。
最有趣的是,这个问题只发生在8GB RAM的机器上,但其他用户的RAM为16GB及以上,他们没有报告任何问题,而且这是一个非常间歇性的问题,这让我怀疑是垃圾收集问题。
GetData()
可以被调用多次,因此_businessObjectTask的第一个延续将只被调用一次,因为_businessObjects将从那时起被填充。
我想Object reference not set to an instance of an object
异常被抛出是因为
_businessObjectTask
为null,无法从null任务继续- 作为参数传入的
items
变量不知何故为空
我的日志文件(748)中的行号指向下面突出显示的那个,而不是指向上面#1而不是#2的lambda表达式。我在Visual Studio中玩过,businessObjectTask.ContinueWith()
后面的每一行都被认为是不同的,也就是说,如果它是lambda表达式中的空引用,它会给748 一个不同的行号
如有任何帮助,我们将不胜感激。
编辑:这与什么是NullReferenceException无关,我该如何修复它?因为这是对空引用的更基本的解释,而这要复杂和微妙得多。
异常
堆栈跟踪的完整详细信息(为了简单起见,使用伪类和命名空间名称进行了编辑)
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
是静态的。
这意味着第一次调用GetData()
时_businessObjects
为空,然后将_businessObjectTask
设置为非空。然后,当调用_businessObjectTask.ContinueWith
时,它是非空的,并且继续进行而没有问题。
然而,如果实例化了上面这个类的第二个实例,则_businessObjects
已经被填充,因此_businessObjectTask
保持为空。然后,当调用_businessObjectTask.ContinueWith
时,在_businessObjectTask
上抛出一个NRE。
我本可以选择几个选项,但我最终将_businessObjectTask
删除为同步方法调用,这意味着我不需要再使用continuation,我设置了_businessObjects
一次。
这是一个同步问题。
您假设_businessObjectTask
总是分配在_businessObjects
之前。
然而,这并不能保证。分配_businessObjects
的延续可能在inline中执行,因此在businessObjectService.GetData(...).ContinueWith(...)
返回之前执行。
// 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);
因此,尽管_businessObjectTask
为空,但有可能_businessObjects
不为空。
如果一个并发线程当时会进入你的GetData
方法,那么它显然不会进入
if (_businessObjects == null) // not entered because it's not null
{
...
}
而是继续
var taskSetLEData = _businessObjectTask.ContinueWith // line 748
这将导致空引用异常,因为CCD_ 25为空。
以下是如何简化代码并解决此同步问题:
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
声明为静态。在您的代码中,静态/非静态字段之间似乎存在混淆。我不知道哪一个适合你。