Search code examples
asp.net-mvctaskfactory

Task.Factory.StartNew does not start when method inside have a new parameter


(Problem solved) I have an MVC application, in my Action:

First case: Task never started.

public ActionResult Insert(NewsManagementModel model) {
            //Do some stuff

            //Insert history  
            //new object NewsHistoryDto  as the parameter
            Task.Factory.StartNew(() => InsertNewsHistory(new NewsHistoryDto {
                UserId = 1234,
                Time = DateTime.Now,
                Id = 1234
            })); 
            return RedirectToAction("Index", "NewsManagement");
        }

Second case: Task run normally

public ActionResult Insert(NewsManagementModel model) {
            //Do some stuff

            //Insert history 
            //object NewsHistoryDto was declared outside
            var history = new NewsHistoryDto {
                UserId = 1234,
                Time = DateTime.Now,
                Id = 1234
            }; 
            Task.Factory.StartNew(() => InsertNewsHistory(history)); 
            return RedirectToAction("Index", "NewsManagement");
        }

My question is: When Task.Factory.StartNew and i put a method into it, the parameter of that method (object) must be declare outside??? Because when i write shortly like the first case, i put the "new" keyword in the parameter and the task never run. Reason: In action, i wanna return view as soon as possible, any other stuff not related to that view will be excute in a task and client no need to wait for completed.

I'm very sorry about my bad english :)

Updated 1: Thanks Panagiotis Kanavos, I used QueueBackgroundWorkItem but the problem still the same, if i declare the object outside, this method run normally. But when i use new keyword inside the parameter, this method never run. No exception, no errors. Can anyone explain to me how this possible :(

Updated 2: I try two case:

First:

    HostingEnvironment.QueueBackgroundWorkItem(delegate {
        var handler = m_bussinessHandler;
        handler.InsertNewsHistoryAsync(new NewsHistoryDto {
            UserId = UserModel.Current.UserId,
            Time = DateTime.Now,
            Id = newsId
        });
    });-> still doens't works

Second:

        var history = new NewsHistoryDto {
            UserId = UserModel.Current.UserId,
            Time = DateTime.Now,
            Id = newsId
        };

        HostingEnvironment.QueueBackgroundWorkItem(delegate {
            var handler = m_bussinessHandler;
            handler.InsertNewsHistoryAsync(history);
        });-> works normally

So where is the problem here??? That's not about m_bussinessHandler because i copied.

Updated 3: I found the reason. The reason is UserModel.Current, this is an object in HttpContext.Current.Session["UserModel"], in this case when i call async method, when this method actually excute, it can access to HttpContext.Current which is null. So i can solve this problem by declare object outside to store data and pass it into method or I capture UserModel.Current and pass it into this method to use UserModel.Current.UserId.

My problem actually solved, thanks everyone for helping me, especially Panagiotis Kanavos.


Solution

  • As it is, your code returns to the caller before the task finishes. The task may not have even started executing by the time the code returns, especially if you are debugging the method. The debugger freezes all threads and executes only one of them step by step.

    Moreover, .NET's reference capture means that when you use m_businessHandler you capture a reference to the m_businessHandler field, not its value. If your controller gets garbage collected, this will result in a NullReferenceException. To avoid this you have to make a copy of the field's value inside your lambda.

    You should write a proper asynchronous method instead, that returns to the user only when the asynchronous operation completes:

    public async Task<ActionResult> Insert(NewsManagementModel model) {
        //Do some stuff
    
        //Insert history  
        //new object NewsHistoryDto  as the parameter
        await Task.Run(() => 
            var handler=m_bussinessHandler;
            handler.InsertNewsHistory(new NewsHistoryDto {
                UserId = 1234,
                Time = DateTime.Now,
                Id = 1234
        })); 
        return RedirectToAction("Index", "NewsManagement");
    }
    

    Task.Run or Task.Factory.StartNew are roughly equivalent.

    Even so, it's not a good idea to use Task.Run to fake asynchronous execution - you simply switched execution from one server thread to another. You should use asynchronous methods all the way down to the database, eg. use ExecuteNonQueryAsync if you are using ADO.NET or SaveChangesAsync in Entity Framework. This way no thread executes or blocks while the code waits for the database call to complete.

    The compiler also takes care of capturing the field's value so you don't need to copy anything. The resulting code is a lot cleaner:

    public async Task<ActionResult> Insert(NewsManagementModel model) {
        //Do some stuff
    
        //Insert history  
        //new object NewsHistoryDto  as the parameter
        await m_bussinessHandler.InsertNewsHistoryAsync(new NewsHistoryDto {
                UserId = 1234,
                Time = DateTime.Now,
                Id = 1234
        }; 
        return RedirectToAction("Index", "NewsManagement");
    }
    

    If you do want the operation to run in the background and return to the client immediately, you can use QueueBackgroundWorkItem which starts a new Task and registers it with IIS. In this case you have to copy the field's value again:

    public ActionResult Insert(NewsManagementModel model) {
        //Do some stuff
    
        //Insert history  
        //new object NewsHistoryDto  as the parameter
        HostingEnvironment.QueueBackgroundWorkItem(ct=>
            var handler=m_bussinessHandler;
            handler.InsertNewsHistoryAsync(new NewsHistoryDto {
                UserId = 1234,
                Time = DateTime.Now,
                Id = 1234
        }); 
        return RedirectToAction("Index", "NewsManagement");
    }
    

    IIS can still cancel the task, eg when the application pool recycles. Before it does so, it will notify the task through the CancellationTask passed to the lambda (the ct parameter). If the task doesn't finish in time, IIS will go on and abort the thread anyway. A long running task should check the token periodically.

    You can pass the cancellation token to most asynchronous methods, eg ExecuteNonQueryAsync(CancellationToken). This will cause the IO operation to cancel as soon as it's safe to do so instead of waiting until the remote server responds.

    Scott Hanselman has a nice article describing all the ways available to rung tasks and even scheduled jobs in the background in How to run Background Tasks in ASP.NET