访问同一对象的两个线程产生意外结果

时间:2015-12-11 11:30:44

标签: c# asp.net entity-framework-6

我今天在系统中发现了一个错误,但我无法理解为什么会发生错误。我要求解释EF6和C#如何在这里工作,因为我觉得我错过了一些非常重要的东西。

我的问题是"这里发生了什么",而不是"如何让它工作"。修复是非常简单的 - 我可以添加一些锁来防止竞争条件,但我只是不明白这是怎么回事。

我使用EF6在C#中使用ASP.NET Web应用程序。设计非常简单 - 我们有用户和任务。用户有平衡,如果非零,他们可以启动任务。任务以异步方式执行,一旦任务完成,应用程序将收到带有结果的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函数是一个运行DB事务的函数,如果用户余额非零并且成功从余额中减少1并将其保存到DB,则该函数返回true。如果有任何问题或余额为零,则该函数返回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构造函数只构建一个从任务数据发送给用户的结构,这里不是一个重要的功能。重要的事实是,当一个新任务被放入DB时,它被初始化为Finished设置为false并且Paid设置为false。我们将Finished设置为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如何看到task.Finished == true并且同时task.Paid == false?

1 个答案:

答案 0 :(得分:0)

线程2在Succeeded方法启动之后但在它完成之前询问了PaidSave属性。此时,很可能(实际上可能)对于从存储库检索的任务,Succeeded可能为真,Paid将为假。

回调需要是事务性的,因此一旦任务完成,其他线程就会被阻止访问它,直到它被更新。