空引用-任务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异常被抛出是因为

  1. _businessObjectTask为null,无法从null任务继续
  2. 作为参数传入的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一次。

空引用-任务ContinueWith()

这是一个同步问题。

您假设_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声明为静态。在您的代码中,静态/非静态字段之间似乎存在混淆。我不知道哪一个适合你。