14
\$\begingroup\$

I have created this extension method that can be used to run a function with a timeout applied to it. Is this a sensible way of doing it?

public static class FuncExtensions
{
    public static T RunUntil<T>(this Func<T> predicate, TimeSpan timeout, string taskName)
    {
        var result = default(T);
        Action runTask = () => result = predicate();

        try
        {
            var task = Task.Factory.StartNew(runTask);
            if (task.Wait(timeout))
            {
                return result;
            }

            throw new TaskTimeoutException(string.Format("'{0}' timed out after {1}", taskName, timeout));
        }
        catch (AggregateException aggregateException)
        {
            throw aggregateException.InnerException;
        }
    }
}

Which can be used like this:

Func<int> task = () => { //some slow service };

var serviceResponse = task.RunUntil(_serviceTimeout, "Name of Task");
\$\endgroup\$
8

3 Answers 3

8
+250
\$\begingroup\$

I think that his question would suit more on SO as it doesn't require explicitly a code review. This question is more about of how to correctly cancel a task instead of fixing apointing good pracrises, etc.. However you have made a great quest that deserves an answer:

if (task.Wait(timeout))
{
    return result;
}

throw new TaskTimeoutException(string.Format("'{0}' timed out after {1}", taskName, timeout));

This will wait timeout milliseconds for the task completion however, the task may still continue, like @Jesse pointed out.

In tasks you are only able to cancel a task with a Cancelation Token Source. With a cancellation token source you will be getting a token that you may then use to cancel the task. That token must be passed through all methods, this also includes the method that makes the long computation you were interested to cancel. This is because that you must actively check if there was a cancelation request. You can do this by calling the ThrowIfCancellationRequested method.

So, in the end, you won't be able to cancel your previous code without at least adding a Cancellation token parameter and check for the cancellation with that.

Here follows the pattern of a method that supports cancellation:

public static int Sum(int[] values, CancellationToken token){
  int acc = 0;
  for(int i = 0; i < values.Length; ++i){
      acc += values[i]; 
      token.ThrowIfCancellationRequested(); //you may wish to do this verification less often, doing this every iteration will reduce performance
      Thread.Sleep(100); //just simulate a longer task so you may know that it may be canceled before completing. 
  }
  return acc;
}

And here is how you could request a cancelation:

public static void Main(string[] args){
   CancellationTokenSource cancelSource = new CancellationTokenSource ();
   CancellationToken token = cancelSource.Token;
   int[] values = Enumerable.Range(0, 1000).ToArray();
   Task.Factory.StartNew(()=> Sum(values, token), token)
     .ContinueWith(t => {
        if(!t.IsCanceled){ // you need to check canceled, otherwise a cancelation exception will be raised when getting Result
           Console.WriteLine(t.Result);
         }
     });
   cancelSource.Cancel();
}

Edit I noticed that I only talked about the cancellation and skipped the timeout, which was your real problem. Well, now that you know about how a task may be canceled you can use a technique similar to the one you were using: Wait with the timeout, and request a cancellation:

int[] values = Enumerable.Range(0, 1000).ToArray();
var src = new CancellationTokenSource();
Task.Factory.StartNew(() => Sum(values, src.Token), src.Token).Wait(timeout);
src.Cancel();
\$\endgroup\$
4
\$\begingroup\$
  • Instead of calling var task = Task.Factory.StartNew(runTask); or like Bruno Costa correctly suggested with a CancellationToken like var task = Task.Factory.StartNew(runTask, cancellationToken); you can here use for NET 4.5 the overloaded Task.Run(Action, CancellationToken) method, which can be seen as a simplified call to Task.Factory.StartNew().

    See also : https://stackoverflow.com/a/22087211

  • By providing a default value for taskName the message of a thrown TaskTimeoutException would look better.

  • Early checking if TimeSpan timeout is in the given range, will reduce overhead.

  • Action runTask should be renamed to runAction or better to just action.

\$\endgroup\$
-1
\$\begingroup\$

Here is how I achieved something similar.

static void Main(string[] args)
        {
            Console.BufferHeight = Int16.MaxValue-1;
            RunWithTaskApproach();
            Console.WriteLine("Started calculating stuff!");
            RunUntil(() => Calculatestuff(), TimeSpan.FromSeconds(10));
            Console.ReadLine();
        }



public static void RunUntil(Action predicate,TimeSpan timeout)
    {
        Thread thread = new Thread(new ThreadStart(predicate));
        thread.Name = "My Little Thready";
        thread.Start();
        Task.Run(() => Thread.Sleep(timeout))
            .ContinueWith((r) => 
            {
                thread.Abort();
                Console.WriteLine("Calculating stuff timed out!"); 
            });
    }

public static void Calculatestuff()
    {
        BigInteger number = new BigInteger(1);
        while(true)
        {
            Console.WriteLine(ShortenNumber(number,20));
            if (number.IsEven)
                number += number + 1;
            else
                number *= 2;
        }
    }

Bruno's answer does the same with tasks with the added benefit that you can get a return value instead of having to use global variables or printing on the console/using a static class and all that. I still decided to post this since I had some fun writing it and I think it does work as an alternate solution.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.