2015-12-11 3 views
0

Я нашел ошибку в нашей системе сегодня, но я не могу понять, почему это происходит. Я прошу объяснить, как здесь работают EF6 и C#, потому что я думаю, что у меня что-то очень важное.Два потока, обращающихся к одному и тому же объекту, создают неожиданный результат

Мой вопрос: «Что здесь происходит», а не «Как заставить его работать». Исправление довольно тривиально - я могу добавить некоторые блокировки, чтобы предотвратить состояние гонки, но я просто не понимаю, как это возможно.

У меня есть веб-приложение ASP.NET в C# с использованием EF6. Дизайн довольно прост - у нас есть пользователи и задачи. Пользователи имеют свой баланс, и если он не равен нулю, они могут запускать задачи. Задачи выполняются асинхронно, и приложение получает обратный вызов HTTP POST с результатами после завершения задачи. Мы разрешаем пользователю активно спрашивать нашу систему о том, завершена ли задача. Если он еще не закончен, мы сообщим пользователю, что он не закончен, если он закончен, мы дадим ему результат.

Может случиться так, что задача не удалась, и в этом случае мы не хотим считать ее балансом пользователя. Вот почему баланс изменяется после того, как мы получим результаты задачи через обратный вызов и только если задача выполнена успешно.

Итак, вот сокращенный код отрезала от рутинной обработки обратного вызова, отображаются только соответствующие строки:

// before this we have extracted taskId and data from the call parameters 

// log Callback: Data received 
Task task = unitOfWork.TaskRepository.Get(t => t.TaskId == taskId).SingleOrDefault(); 
if (task != null) 
{ 
    task.Finished = true; 
    task.Succeeded = success; 
    task.Data = data; 

    if (task.Succeeded) 
    { 
     // log Callback: BillUser 
     if (BillUser(task.UserId)) 
     { 
      // log Callback: BillUser succeeded 
      task.Paid = true; 
     } 
     else 
     { 
      // log Callback: BillUser failed 
      task.Paid = false; 
     } 
    } 
    else 
    { 
     // log Callback: Task failed 
     task.Paid = true; 
    } 

    unitOfWork.TaskRepository.Update(task); 
    // log Callback: Saving 
    unitOfWork.Save(); 
    // log Callback: Saved 

    // log Callback: End 
} 

Это очень простой код. Функция BillUser - это функция, выполняющая транзакцию БД, и функция возвращает true, если баланс пользователя не равен нулю, и если ему удалось уменьшить 1 из баланса и сохранить его в БД. Если есть какая-либо проблема или баланс равен нулю, функция возвращает false.

Мы используем единицу концепции работы с родовым хранилищем, поэтому Get метод выглядит так, как в здесь: http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application

Аналогично методу обновления и сохранения можно найти в этой статье.

UnitOfWork принадлежит к контроллеру и инициализируется следующим образом:

public class TaskController : Controller 
{ 
    private UnitOfWork unitOfWork = new UnitOfWork(); 

этот контроллер реализует оба - метод обработки обратного вызова, из которого выше код, а также второй соответствующий метод, который обрабатывает пользователь запросы о том, завершена ли задача. Этот метод выглядит следующим образом:

public ActionResult GetResult(string taskId) 
{ 
    // log GetResult: Start 
    Task task = unitOfWork.TaskRepository.Get(t => t.TaskId == taskId).SingleOrDefault(); 

    TaskResponse response = null; 
    if (task != null) 
    { 
     if (task.Finished) 
     { 
     response = new TaskResponse(task); 
     if (response.Succeeded) 
     { 
      // log GetResult: Task succeeded 

      if (task.Paid) 
      { 
       // log GetResult: Task paid 
      } 
      else 
      { 
       // log GetResult: Task unpaid 
      } 
     } 
     else 
     { 
      // log GetResult: Task failed 
     } 
     } 
     else 
     { 
     // log GetResult: Task not finished 
     } 
    } 
    // log GetResult: End 

TaskResponse конструктор только создает структуру, чтобы отправить пользователь из данных Целевых, это не является важной функция здесь. Важным фактом является то, что когда новая задача помещается в БД, она инициализируется с параметром «Готово» равным false, а Paid - false. Единственное место, где мы установили Finished to true, находится в коде выше.

В чем проблема?

Большую часть времени приложение работает, но сегодня, я нашел эту последовательность в журнале:

thread 1: Callback: Data received 
thread 1: Callback: BillUser 

thread 2: GetResult: Start 
thread 1: Callback: BillUser succeeded 

thread 1: Callback: Saving 
thread 2: GetResult: Task succeeded 
thread 2: GetResult: Task unpaid 
thread 1: Callback: Saved 

thread 1: Callback: End 
thread 2: GetResult: End 

Баланс пользователя был отличен от нуля в любой момент времени. Из того, что я думал о том, как работает EF, это не должно быть возможным.

Может кто-нибудь объяснить, что произошло? Как может поток 2 увидеть задачу. Исправлено == true и в то же время task.Paid == false?

+0

Не уверен в вашей конкретной проблеме, но если вы не делаете больше в своей процедуре обратного вызова, почему вы сохраняете каждый раз. Как и в этом, вы чувствуете, что спасаете, даже когда работа еще не закончена. Вы действительно это делаете. Кроме того, похоже, что вы всегда переопределены. Преуспеть перед тем, как проверить, что работа уже выполнена. Обратите внимание, что ничто из этого не помогает в вашей проблеме, просто наблюдая за показанным кодом. – TYY

+0

Когда вызывается процедура обратного вызова, задача всегда завершается. Это может потерпеть неудачу или преуспеть, но это всегда закончено. Задача. Преемственность также правильно установлена, переменная успеха заполняется кодом, который не отображается. – Wapac

ответ

0

Тема 2 опросила объекты Succeeded и Paid после того, как начался метод Save, но до его завершения. На данный момент вполне возможно (действительно, вероятно), что для задачи, полученной из репозитория, Succeeded может быть истинным, а Paid будет ложным.

Обратный вызов должен быть транзакционным, так что, как только задача завершается, другие потоки блокируются от доступа к нему до тех пор, пока он не будет обновлен.

+0

Я не думаю, что Судебный процесс имеет значение. У меня проблема с Finished, а не с преуспеванием - по крайней мере, я так думаю. Мой вопрос о том, как Finished is true + Paid является ложным одновременно, а не преуспел. Или я что-то пропустил в вашем ответе? Другими словами - почему второй поток не записывал «GetResult: Task not finished»? – Wapac

+0

Да, на самом деле, снова просматривая ваш код, я думаю, что единственный способ получить «Законченный» true и «Paid» false: если a) ваши два объекта задачи ссылаются на один и тот же объект (попробуйте вывести его из адреса или поставить GUID в объекте во время 'Get' и регистрации из обоих потоков) или b) если что-то еще где-то устанавливает' Finished' в true. –

+0

TaskId уникален и да, оба потока DO касаются одной и той же задачи. Вы предлагаете, чтобы EF предоставил одну и ту же ссылку (указатель на память компьютера) на оба потока, чтобы они действительно перезаписывали друг друга еще до обновления/сохранения? Другими словами, ситуация может быть решена даже без блокировок, просто переместив «task.Finished = true» непосредственно перед вызовом Update? Или, другими словами, EF не дает мне новый экземпляр объекта каждый раз, когда я использую Get()? – Wapac

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