2015-12-09 2 views
3

Для следующего фрагмента кода (.NET v4.0.30319) Я получаю исключение нулевой ссылки, указанное ниже во втором продолжении.Ссылка на нуль - Задача ПродолжитьWith()

Особенно интересно, что эта проблема произошла только в машинах с 8 ГБ ОЗУ, а у других пользователей - 16 ГБ и более, и они не сообщили о какой-либо проблеме, и это очень прерывистая проблема, которая заставляет меня подозревать проблему сбора мусора.

GetData() можно назвать несколько раз, поэтому первое продолжение _businessObjectTask будет вызываться только один раз, поскольку _businessObjects будет заполнен с этой точки вперед.

я себе Object reference not set to an instance of an object исключение бросают, потому что либо

  1. _businessObjectTask является недействительным и не может продолжаться от нулевой задачи.
  2. items переменная, которая передается в качестве параметра равно нулю как-то

Номер строки в файле протокола (748) указывает на подсвеченный снизу, а не лямбда-выражение, которое указывает на # 1 выше, вместо # 2. Я играл в Visual Studio, и каждая из строк, следующих за businessObjectTask.ContinueWith(), считается другой, т. Е. Если это была нулевая ссылка в выражении лямбда, она выдавала бы другой номер строки до 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 имеет значение null при первом вызове GetData(), который затем устанавливает _businessObjectTask в ненулевое значение. Затем, когда вызывается _businessObjectTask.ContinueWith, он не равен нулю и продолжается без проблем.

Однако, если экземпляр второго экземпляра этого класса указан, _businessObjects уже заселен, поэтому _businessObjectTask остается недействительным. Затем, когда вызывается _businessObjectTask.ContinueWith, бросается NRE на _businessObjectTask.

Было несколько вариантов, которые я мог бы взять, но я закончил удаление _businessObjectTask синхронным вызовом метода, что означает, что мне больше не нужно использовать продолжение, и я установил _businessObjects один раз.

+0

Является ли 'GetData' вызываемым несколькими потоками параллельно? –

+0

@Ciaran Martin, вы уверены, что вы разместили код целиком? У вас явно есть гонка, но я не вижу никаких ожиданий между проверкой '_businessObjectTask' за нуль и установкой ее, а вызов' ContinueWith', * или * любой настройки кода '_businessObjectTask' возвращается к нулевому –

+0

@ Кирилл Шленский. Я не вложил весь код, но все соответствующие части есть. Я обновил вопрос и теги, чтобы указать, что я использую .NET 4.0, что означает, что async и ждут недоступны. –

ответ

1

Это проблема синхронизации.

Вы предполагаете, что _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);    

Поэтому вполне возможно, что _businessObjects не равно нулю, хотя _businessObjectTask является нулевым.

Если одновременно поток будет ввести свой метод GetData в то время это было бы явно не войти

if (_businessObjects == null) // not entered because it's not null 
{ 
    ... 
} 

... и вместо того, чтобы продолжить

var taskSetLEData = _businessObjectTask.ContinueWith // line 748 

... что вызовет нуль ссылка исключение с _businessObjectTask - null.


Вот как вы могли бы упростить код и решить эту проблему синхронизации:

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) для выполнения inline, но даже если это произойдет, ваша теория не согласуется с трассировкой стека, опубликованной выше (см. 'ApplicationNamespace.ClassName \' 1. <> C__DisplayClass4e. b__44 (Задача \ '1 t)'). Я плохо разбираюсь в трассировке стека, так что может быть совершенно неправильно. –

+0

Не имеет значения, насколько маловероятно, чтобы это произошло. Если это может произойти, вы можете быть уверены, что он * будет иногда делать это, особенно на машине вашего клиента в производственной среде :-) –

+0

Я не понимаю, почему вы думаете, что трассировка стека будет противоречить моему ответу ? –

Смежные вопросы