2

I am trying to make a simple program to learn how to use cancellation tokens, but they seem to be giving me plenty of problems. By adjusting the CancelAfter value, I can absolutely stop a task dead, but IsCancellationRequested is not ever calling for me. Since CancelAfter seems to be executing properly, it's very confusing to me that IsCancellationRequested fails 100% of the time.

Here's the code: (note, it's a stripped down example)

The function that initializes the task:

void IPCheck() {
    var cToken = new CancellationTokenSource();
    cToken.CancelAfter(1000);

    string tempIP = "123.123.123.123";

    Task.Factory.StartNew(() => PingTask(tempIP, cToken.Token), cToken.Token);
}

The task that is successfully(?) canceled, but doesn't have IsCancellationRequested called.

void PingTask(string addressString, CancellationToken cancelToken) {

    Ping currentPing = new Ping();
    PingReply replyPing;
    replyPing = currentPing.Send(addressString);

    string returnString = "Return String Unmodified." + Environment.NewLine;


    if (replyPing.Status == IPStatus.Success) {
        returnString = "Ping to " + addressString + " successful." + Environment.NewLine);
    }

    else if (replyPing.Status == IPStatus.TimedOut) {
        returnString = "Ping to " + addressString + "timed out." + Environment.NewLine);
    }

    if (cancelToken.IsCancellationRequested) {
        returnString = "Cancellation Requested for " + addressString + Environment.NewLine);
    }

    TaskHelperReturnString(returnString);
    return;
}

Unless it succeeds or times out, while CancelAfter is set to something like 10000, returnString always writes "Return String Unmodified."

A successful ping will be over in an instant. With CancelAfter(1000) there isn't enough time for it to timeout and nothing is printed, including TaskHelperReturnString().

The IP I used always fails, so it's good for testing.

soxroxr
  • 297
  • 2
  • 7
  • 2
    You cancel after a second. The task probably has long been completed by that time. – Sefe Oct 24 '17 at 08:22
  • If the ping is successful, yes. If not, the task typically lasts 3-5 seconds. This does successfully cancel it, as nothing is printed in the output via `TaskHelperReturnString()` – soxroxr Oct 24 '17 at 08:24
  • @Sefe ping does not take 3-5 seconds, resolving host name usually takes long, you are probably getting `IPStatus.BadDestination` or something else, and you have only compared for `Success` and `TimeOut`, print value of `replyPing.Status` – Akash Kava Oct 24 '17 at 08:35
  • That's entirely possible, however in the event that neither of those occur, it prints "Return String Unmodified." - I'm not looking to make a comprehensive piece of pinging software, I'm testing Cancellation Tokens. However, when CancelAfter occurs before the task has completed, nothing is printed. – soxroxr Oct 24 '17 at 08:45
  • You aren't waiting for the task anywhere. The cancellation source is probably already garbage collected by the time you access the *token* it created. Why aren't you using `SendPingAsync` and `async/await` ? They are available in all *supported* .NET versions, ie .NET 4.5.2 and later. – Panagiotis Kanavos Oct 24 '17 at 08:55
  • I came upon this question as I am having an issue with `CancelAfter` working but `IsCancellationRequested` not ever being set to true as well. – ttugates Nov 05 '18 at 22:21
  • @ttugates in that case post a question with your code. There's nothing wrong with CancelAfter. If there was, people would have noticed in the 8 years since it was introduced. – Panagiotis Kanavos Nov 06 '18 at 07:48

1 Answers1

0

In all supported .NET versions (4.5.2+) you can use async/await and SendPingAsync to send a ping asynchronously without requiring Task.Run. Even without them, you don't need the cancellation source, as SendAsync(IPAddress,int) accepts a timeout parameter.

Your code could look like this :

async Task<string> IPCheck()
{
    Ping currentPing = new Ping();
    var replyPing= await currentPing.SendPingAsync(addressString,1000);        
    switch(replyPint.Status)
    {
        case IPStatus.Success:
            var okString = $"Ping to {addressString} successful.\n";
            SuccessActions(0, arrayIndex);
            return okString;
        case IPStatus.Timeout:
            return  $"Ping to {addressString} timed out.\n";
        default:
            // ????
    }
}

Even if you wanted a larger task timeout from the ping timeout (why?) you could use Task.Delay and Task.WhenAny instead of a cancellation source. A task timeout shorter than the ping timeout wouldn't make much sense, as you'd never get a ping timeout this way.

Using Task.Delay :

async Task<string> IPCheck()
{
    Ping currentPing = new Ping();

    var pingTask= currentPing.SendPingAsync(addressString,1000);        
    var delayTask=Task.Delay(2000);
    var finished =  await Task.WhenAny(delayTask,pingTask);
    if (finished == delayTask)
    {
        return "Long timeout!"
    }

    var replyPing=await pingTask;

    switch(replyPing.Status)
    {
        case IPStatus.Success:
            var okString = $"Ping to {addressString} successful.\n";
            SuccessActions(0, arrayIndex);
            return okString;
        case IPStatus.Timeout:
            return  $"Ping to {addressString} timed out.\n";
        default:
            // ????
    }
}

UPDATE

The original code can't work because the task is never awaited. The method where the cancellation source is defined exits immediatelly, forcing a garbage collection on the source. The source is not preserved even though the token is passed to the task. The token is. Since the source no longer exists, it can't notify any of the tokens it generated.

To make the original code work with a token source, the task itself has to be awaited or the cancellation source should have a larger scope than the method, ie it should be a field.

To make a proper example, assume we wanted 5 pings before responding Eg :

Update 2018-11-06 Added proper disposal of CancellationTokenSource to avoid flooding the timer queue. Explanation here

static async Task<string> IPCheck(string address)
{
    using (var cts = new CancellationTokenSource(2000))
    {
        var result = await SendPings(address, 5, cts.Token);
        return result;
    }
}


static async Task<string> SendPings(string address, int count, CancellationToken token)
{
    var responses = new List<PingReply>(5);

    Ping currentPing = new Ping();

    for (int i = 0; i < count; i++)
    {
        if (token.IsCancellationRequested)
        {
            return "Cancelled!";
        }

        var replyPing = await currentPing.SendPingAsync(address, 1000);
        switch (replyPing.Status)
        {
            case IPStatus.Success:
                responses.Add(replyPing);
                break;
            case IPStatus.TimedOut:
                responses.Add(replyPing);
                Console.WriteLine($"Ping to {address} timed out.\n");
                break;
            default:
                responses.Add(replyPing);
                break;
        }
    }
    var avgTime = responses.Average(resp => resp.RoundtripTime);
    return $"All pings returned. Average roundtrip {avgTime}";
}

In this case the IPCheck method won't block, but won't finish either until either all 5 pings finish in 2 seconds, or a timeout (ping or overall) occurs.

If the ping takes too long and the overall timeout is too short, IsCancellationRequested becomes true and Cancelled is returned.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • "I am trying to make a simple program to learn how to use cancellation tokens," – soxroxr Oct 24 '17 at 09:09
  • @soxroxr you picked the wrong example then. Although this code shows what's wrong - you used `StartNew`, never *awaited* the task, exited before the task finished and forced the source to be garbage collected – Panagiotis Kanavos Oct 24 '17 at 09:11
  • @soxroxr added an example that shows how to use a CTS to add an overall timeout over multiple pings – Panagiotis Kanavos Oct 24 '17 at 09:21
  • This answer doesn't address the original question. I am finding `CancellationTokenSource.CancelAfter()` doesn't set `CancellationTokenSource.Token.IsCancellationRequested` to `true` and is why I am looking at this question. – ttugates Nov 05 '18 at 22:27
  • @ttugates this answers the OP's question, which didn't work due to a bug in the code itself. You have *another* question though, about a different method. Most likely it's a problem with the code [as in this related question](https://stackoverflow.com/questions/17717625/cancellationtokensource-cancelafter-not-working). That code didn't await the task either, not even checked the token. You'll have to post a question with your code if you want specific help – Panagiotis Kanavos Nov 06 '18 at 07:39
  • @ttugates btw *this* code works, and does raise the `IsCancellationRequested` flag provided the overall timeout is short enough and the ping takes long enough to allow the CTS to trigger. Do you dispose the CTS? If you don't you may be flooding the timer queue – Panagiotis Kanavos Nov 06 '18 at 08:08