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
  • 1
    \$\begingroup\$ I don't see the code which should stop the task if it goes over on time. \$\endgroup\$ Commented Sep 21, 2012 at 13:15
  • \$\begingroup\$ The if(task.Wait(timeout)) line as documented here: msdn.microsoft.com/en-us/library/dd235606.aspx \$\endgroup\$ Commented Sep 21, 2012 at 14:43
  • 6
    \$\begingroup\$ Yes, but it doesn't cancel the executing task if you go over the wait time. It'll still be running. \$\endgroup\$ Commented Sep 21, 2012 at 14:44
  • 1
    \$\begingroup\$ How to do the cancelling is described here Task Cancellation and additionaly How to: Cancel a Task and Its Children \$\endgroup\$ Commented Oct 15, 2012 at 10:10
  • \$\begingroup\$ This article describes the kind of problem I was dealing with: blogs.msdn.com/b/pfxteam/archive/2012/10/05/… \$\endgroup\$ Commented Oct 15, 2012 at 12:59

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\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.