Home > Net >  Return data from long running Task on demand
Return data from long running Task on demand

Time:03-09

I want to create a Task, which may run for many minutes, collecting data via an API call to another system. At some point in the future I need to stop the task and return the collected data. This future point is unknown at the time of starting the task.

I have read many question about returning data from tasks, but I can't find any that answer this scenario. I may be missing a trick, but all of the examples actually seem to wait in the man thread for the task to finish before continuing. This seems counter-intuitive, surely the purpose of a task is to hand off an activity whilst continuing with other activities in your main thread? Here is one of those many examples, taken from DotNetPearls..

namespace TaskBasedAsynchronousProgramming
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Main Thread Started");
            Task<double> task1 = Task.Run(() => 
            {
                return CalculateSum(10);
            });
            
            Console.WriteLine($"Sum is: {task1.Result}");
            Console.WriteLine($"Main Thread Completed");
            Console.ReadKey();
        }
        static double CalculateSum(int num)
        {
            double sum = 0;
            for (int count = 1; count <= num; count  )
            {
                sum  = count;
            }
            return sum;
        }
    }
}

Is it possible to do what I need, and have a long-running task running in parallel, stop it and return the data at an arbitrary future point?

CodePudding user response:

Here is a sample application how you can do that:

static double partialResult = -1;
static void Main()
{
    CancellationTokenSource calculationEndSignal = new(TimeSpan.FromSeconds(3));
    Task meaningOfLife = Task.Run(() => 
         GetTheMeaningOfLife(calculationEndSignal.Token), 
         calculationEndSignal.Token);
    calculationEndSignal.Token.Register(() => Console.WriteLine(partialResult));
    Console.ReadLine();
}

static async Task GetTheMeaningOfLife(CancellationToken cancellationToken)
{
    foreach (var semiResult in Enumerable.Range(1, 42))
    {
        partialResult = semiResult;
        cancellationToken.ThrowIfCancellationRequested();
        await Task.Delay(1000);
    }
}
  • partialResult is a shared variable between the two threads
    • The worker thread (GetTheMeaningOfLife) only writes it
    • The main thread (Main) only reads it
    • The read operation is performed only after the Task has been cancelled
  • calculationEndSignal is used to cancel the long-running operation
    • I've have specified a timeout, but you can call the Cancel method if you want
  • meaningOfLife is the Task which represents the long-running operation call
  • I have passed the CancellationToken to the GetTheMeaningOfLife and to the Task.Run as well
    • For this very simple example the Task.Run should not need to receive the token but it is generally a good practice to pass there as well
  • Register is receiving a callback which should be called after the token is cancelled
  • ReadLine can be any other computation
    • I've used ReadLine to keep the application running
  • GetTheMeaningOfLife simply increments the partialResult shared variable
    • either until it reaches the meaning of life
    • or until it is cancelled

CodePudding user response:

Here is one approach. It features a CancellationTokenSource that is used as a stopping mechanism, instead of its normal usage as a cancellation mechanism. That's because you want to get the partial results, and a canceled Task does not propagate results:

CancellationTokenSource stoppingTokenSource = new();

Task<List<int>> longRunningTask = Task.Run(() =>
{
    List<int> list = new();
    for (int i = 1; i <= 60; i  )
    {
        if (stoppingTokenSource.IsCancellationRequested) break;

        // Simulate a synchronous operation that has 1 second duration.
        Thread.Sleep(1000);
        list.Add(i);
    }
    return list;
});

Then, somewhere else in your program, you can send a stopping signal to the task, and then await asynchronously until the task acknowledges the signal and completes successfully. The await will also propagate the partial results:

stoppingTokenSource.Cancel();
List<int> partialResults = await longRunningTask;

Or, if you are not in an asynchronous workflow, you can wait synchronously until the partial results are available:

stoppingTokenSource.Cancel();
List<int> partialResults = longRunningTask.Result;
  • Related