3

My other question basically needed this so I thought I would share my generic solution here.

I had trouble with HttpClient Tasks for web requests basically never completing; so the program or thread hangs. I needed a simple way to add a timeout to a task, so that it returns normally or returns cancelled if the timeout expires first.

Some alternative approaches can be found here: http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx

Elliot
  • 2,002
  • 1
  • 20
  • 20
  • I've voted to close because it's not good practice to post and answer your own question. Please consider moving this to codereview.stackexchange.com if your intent was to show off a clever code fragment and get feedback on it – Adrian Zanescu Nov 27 '13 at 13:30
  • 1
    AZ have you posted a question recently? It offers you to "share your knowledge" by answering your own question. – Elliot Nov 27 '13 at 13:33
  • 5
    @AZ. What makes you think it's not good practice to post and answer your own question? There's even an "Answer your own question" checkbox in the "Ask Question" form. – dcastro Nov 27 '13 at 13:33
  • 2
    @AZ: If you think it is bad practice you should probably discuss your viewpoint on [Meta](http://meta.stackexchange.com/questions/17845/etiquette-for-answering-your-own-question). The current implementation actually [supports answering your own question](http://meta.stackexchange.com/questions/132886/what-is-this-answer-your-own-question-jazz). – Martin Liversage Nov 27 '13 at 13:36
  • Or you can use a simple solution like this http://stackoverflow.com/a/11191070/1659828 – Honza Brestan Nov 27 '13 at 13:42
  • There is a difference between asking something, not getting a good reply and coming back later after more research to answer your own question and posting the question and the answer at the same time. I've voted to close because i believe that codereview.stackexchange might be a better place for this kind of stuff. Feel free to not back my vote if you feel different. – Adrian Zanescu Nov 27 '13 at 13:44
  • And if you read Honza's comment it appears this was actually asked and answered before. Another reason to close it – Adrian Zanescu Nov 27 '13 at 13:46
  • 4
    @AZ. but he's not asking for a code review, he's sharing knowledge. He claims he found a solution to a common problem, to which there was no satisfactory response on stack overflow. Whether the claim is valid or not, that's a different question. – dcastro Nov 27 '13 at 13:46
  • I think you should explain in more detail what the question actually is. Without it, the answer is pretty much useless. – svick Nov 27 '13 at 14:08
  • Did you consider setting [HttpClient.Timeout](http://msdn.microsoft.com/en-us/library/system.net.http.httpclient.timeout(v=vs.110).aspx)? Seems like that'd be a whole lot easier. – Jim Mischel Nov 29 '13 at 22:33
  • Jim, of course I did. I think it just calls Abort on an HttpWebRequest object. – Elliot Nov 30 '13 at 21:54
  • I have no idea why this question is "on hold" but it was pretty successful while it lasted. Thanks Servy for the answer. – Elliot Dec 06 '13 at 13:16

2 Answers2

3

You technically can't take a task and force it to be canceled after some period of time. The best that you can do is create a new task that will be marked as canceled after the given period of time, or will be completed when the other task is if it finishes in time.

A key point to note is that the operation timing out doesn't stop the task from continuing on with it's work, it just fires off all continuations of this new task "early" if the timeout is reached.

Doing this is pretty trivial, thanks to the use of CancellationToken:

var newTask = task.ContinueWith(t => { }
    , new CancellationTokenSource(timeoutTime).Token);

We can combine this patter with WhenAny to easily ensure that any exceptions are properly propagated through to the continuation. A copy for tasks with/without a result is also needed:

public static Task WithTimeout(Task task, TimeSpan timeout)
{
    var delay = task.ContinueWith(t => { }
        , new CancellationTokenSource(timeout).Token);
    return Task.WhenAny(task, delay).Unwrap();
}
public static Task<T> WithTimeout<T>(Task<T> task, TimeSpan timeout)
{
    var delay = task.ContinueWith(t => t.Result
        , new CancellationTokenSource(timeout).Token);
    return Task.WhenAny(task, delay).Unwrap();
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • But the continuewith only occurs if the underlying task is completed! I don't think that this guarantees completion. Unless I'm wrong about that... – Elliot Nov 27 '13 at 17:24
  • ContinueWith: "The returned Task will not be scheduled for execution until the current task has completed, whether it completes due to running to completion successfully, faulting due to an unhandled exception, or exiting out early due to being canceled." http://msdn.microsoft.com/en-us/library/dd270696(v=vs.110).aspx – Elliot Nov 27 '13 at 17:25
  • @Elliot It won't complete normally until the underlying task is completed. It will be cancelled if the cancellation token indicates that it should be cancelled. That's the whole *point* of a cancellation token. If a cancellation token being marked as canceled never cancelled a task there would be no point in ever passing one in. – Servy Nov 27 '13 at 17:28
  • Yeah but it doesn't timeout the underlying task so it's not a solution. – Elliot Nov 27 '13 at 17:30
  • 2
    @Elliot Your solution doesn't either. It's *impossible* to solve that in the general case, as I said in response to your solution. This answer does what your answer does, but in one line of code instead of 45. They are functionally identical though. – Servy Nov 27 '13 at 17:33
  • Sorry Servy, I know that the unresizeable box is annoying but you should read my solution again. – Elliot Nov 27 '13 at 17:38
  • @Elliot I have. You create a TCS, and if the task completes normally, is cancelled on it's own, or throws an exception, you propagate it's state, if the given time passes then you cancel the TCS. This is functionally doing the same thing. If you wanted, you could throw this into a method to shorten it further, but it's so short as it is I don't know if I'd even bother. What gives you the impression that your method in any way cancels the underlying task when the timeout is reached? It doesn't. – Servy Nov 27 '13 at 17:42
  • I didn't mean that it cancels the underlying; a never ending task will still be never ending. What I needed was for this situation not to block the program when the task is awaited. A continuewith only runs (or cancels) if the underlying completes. It's not the same. – Elliot Nov 27 '13 at 17:46
  • @Elliot No, that's not true. The task resulting from the continuation will be marked as cancelled as soon as the cancellation token says it is canceled, even if the task is it a continuation of has not finished yet. – Servy Nov 27 '13 at 17:48
  • Thanks Servy, I need to test it out before ticking your answer. – Elliot Nov 27 '13 at 17:51
  • Thanks again; I wouldn't have guessed that a continuation could complete before its underlying. I think you should post your answer here too:http://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout – Elliot Nov 27 '13 at 18:06
  • @Elliot If you found that question why post your own? In any case, the top answer there is fine. – Servy Nov 27 '13 at 18:09
  • Basically that method would have had async and await keywords and would have returned Task>. I saw that method in the article i linked to at the top. The Cancellation for contiuations must have been added since that article I guess. – Elliot Nov 27 '13 at 18:35
  • @Elliot Continuations have supported cancellation since the beginning. It's not new. As I said in my earlier comment, there'd be no point in accepting them in the first place unless they acted this way; they'd never do anything useful otherwise. – Servy Nov 27 '13 at 18:36
  • ..but as you have showed me there is the Unwrap method... – Elliot Nov 27 '13 at 18:36
  • Note!!!!!! This sample code is not correct. It has a racing issue here. Considering this case, the original task met an exception, the delay task is continued from the original task. The Task.WhenAny may have a racing case that the delay task is returned first, and then it will swallow the exception in the original task. Consider to use Task.Delay for the timeout check, and further check the returned task of Task.WhenAny() method. Refer to this answer: https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout – Yingqin Jun 17 '19 at 02:34
  • @Yingqin If the delay finishes before the original task faults then the expected behavior is to return the cancelled task. Yes, there is a race condition in which the main task races with the timer to see if it finishes before or after the delay, but that's inherent to the operation being requested, and it's not *possible* to eliminate it. The answer you've linked has exactly the same behavior. If the delay finishes before the original task, you get a canceled task, not the result of the original task. – Servy Jun 17 '19 at 13:20
0

You can use CancellationTokenSource when the timeout come:

var DefualtTimeout = 5000;
var cancelToken = new CancellationTokenSource();

var taskWithTimeOut = Task.Factory.StartNew( t =>
                           {
                              var token = (CancellationToken)t;
                              while (!token.IsCancellationRequested)
                              {
                                 // Here your work
                              }
                              token.ThrowIfCancellationRequested();
                           }, cancelToken.Token, cancelToken.Token);

Approach 1:

   if (!taskWithTimeOut.Wait(DefualtTimeout, cancelToken.Token)) 
      {
         cancelToken.Cancel();
      }

Approach 2:

SpinWait.SpinUntil(() => taskWithTimeOut.IsCompleted, new TimeSpan(0, 0, 0, 0, DefualtTimeout ));

Approach 3:

You can make the method async and before the task write await taskWithTimeOut.Wait.. in this way the UI will not blocked.

Bassam Alugili
  • 16,345
  • 7
  • 52
  • 70