空引用 - Task ContinueWith()

时间:2015-12-09 10:56:32

标签: c# asynchronous .net-4.0 task

对于下面的一段代码(.NET v4.0.30319),我在第二个延续中得到了一个空引用异常。

最有趣的是,这个问题只发生在8GB内存的机器上,但是其他用户只有16GB甚至更多,他们没有报告任何问题,这是一个非常间歇性的问题,导致我怀疑垃圾收集问题。

可以多次调用GetData(),因此_businessObjectTask的第一个延续只会被调用一次,因为_businessObjects将从那一点开始填充。

我想是因为

而引发Object reference not set to an instance of an object异常
  1. _businessObjectTask为空,无法从空任务继续。
  2. 作为参数传入的
  3. items变量以某种方式为null
  4. 我的日志文件(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一次。

1 个答案:

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