2015-12-09 72 views
3

對於下面的一段代碼(.NET v4.0.30319),我得到了下面第二個繼續中指出的空引用異常。空引用 - 任務ContinueWith()

最有趣的是,這個問題只發生在8GB內存的機器上,但其他用戶擁有16GB或更多,他們沒有報告任何問題,這是一個非常間歇性的問題,導致我懷疑垃圾收集問題。

GetData()可以被多次調用,所以_businessObjectTask的第一個繼續只會被調用一次,因爲_businessObjects將從該點向前填充。

我想象被拋出的異常Object reference not set to an instance of an object,因爲無論

  1. _businessObjectTask爲null,並且它不能從空任務繼續。被傳遞作爲參數
  2. items變量爲空以某種方式

在我的日誌文件中的行號(748)指向所述一個下面和未突出顯示lambda表達式指向#1的上方,而不是#2。我已經在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

GetData是否被多個線程並行調用? –

+0

@Ciaran Martin,您是否確定*您已完整發布代碼?你很清楚有一場比賽,但是在檢查'_businessObjectTask' for null和設置它之間,以及對'ContinueWith',*或*任何代碼設置_businessObjectTask的調用之間,我沒有看到任何'await'返回null –

+0

@ KirillShlenskiy我沒有粘貼整個代碼,但所有相關的部分都在那裏。我更新了問題和標籤,聲明我使用.NET 4.0,這意味着異步和等待不可用 –

回答

1

這是一個同步問題。

您假設_businessObjectTask總是在_businessObjects之前分配。

但是,不能保證。有可能您的延續執行_businessObjects執行內嵌因此之前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);    

因此,有可能_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爲靜態。在你的代碼中,似乎有靜態/非靜態字段之間的混合。我不知道哪個適合你。

+0

對於異步方法*和*其延續(沒有ExecuteSynchronously標記的計劃),它們似乎都不可能以內聯方式執行,但即使發生這種情況,您的理論似乎也不符合上面提到的堆棧跟蹤(請參閱'ApplicationNamespace.ClassName \'1。<> c__DisplayClass4e。 b__44(Task \'1 t)')。儘管我在閱讀堆棧跟蹤方面不好,所以可能完全錯誤。 –

+0

不管它發生的可能性如何。如果它可能發生,那麼你可以保證它偶爾會這樣做,特別是在生產環境中的客戶機器上:-) –

+0

我不明白你爲什麼認爲棧跟蹤與我的答案相矛盾? –

相關問題