2

I've got a function in a 3rd party library that occasionally goes rogue and never returns. Something like so:

    // This is 3rd party function so I can't make it take a cancellation token.
    public void RogueFunction()
    {

        while (true)
        {
            _logger.LogInformation("sleeping...");
            Thread.Sleep(100);
        }
    }

I'd like to wrap it in a task with a timeout which is easy enough to do with a 'task.Wait(mills)'. While this returns control to me after the timeout, it doesn't actually kill the task.

In the code below, the rogue function continues to log after the timeout.

    [Fact]
    public void Test()
    {
        var task = Task.Factory.StartNew(RogueFunction);
        var complete = task.Wait(500);
        if (!complete)
        {
            // how do I kill the task so that it quits logging?
            Thread.Sleep(5000);

            task.Dispose();  // Throws exception: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
        }
    }

How do I completely kill this task, so that I can retry it without ending up with a bunch of them running infinitely on my background thread pool.

DanielEli
  • 3,393
  • 5
  • 29
  • 36

2 Answers2

2

It seems that Thread.Abort is your only option. If you are afraid that doing so may leave the application in a corrupted state (open file handles etc), then the safest option is to run the thread in a different process, and then kill the process. Another workable solution is to run the thread in a different AppDomain, and then abort the thread and unload the AppDomain.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Hmm.. I was trying to avoid that but thanks for confirming that I'm not missing something from the task based approach. – DanielEli Apr 27 '19 at 23:49
  • Yes, as far as I know Tasks are based on the principal of cooperation. When something goes rogue, there is no polite way of making it behave. – Theodor Zoulias Apr 27 '19 at 23:55
  • @TheodorZoulias - I would have lead with the "different process" option as it is the only safe way to go. Even using a separate `AppDomain` isn't safe when calling `Thread.Abort()` - it can corrupt the calling `AppDomain` too. – Enigmativity Apr 28 '19 at 12:45
  • @Enigmativity agreed. Thread.Abort is the easiest to implement though. Sometimes you can be sure that aborting a thread is safe, because you can inspect the source code and assert that no unmanaged resources are going to leak. – Theodor Zoulias Apr 28 '19 at 13:24
1

UPDATED:

Let one has such a function:

static class Rogue
{
    // This is 3rd party function so I can't make it take a cancellation token.
    public static void RogueFunction()
    {
        while (true)
        {
            Console.WriteLine("RogueFunction works");
            Thread.Sleep(1000);
        }
    }
}

Possible solution is to wrap it with a class like this:

public class InfiniteAction
{
    private readonly Action action;
    private CancellationTokenSource cts;
    private Thread thread;

    public InfiniteAction(Action action) => this.action = action;

    public void Join() => thread?.Join();

    public void Start()
    {
        if (cts == null)
        {
            cts = new CancellationTokenSource();
            thread = new Thread(() => action());
            thread.IsBackground = true;
            thread.Start();
            cts.Token.Register(thread.Abort);
        }
    }

    public void Stop()
    {
        if (cts != null)
        {
            cts.Cancel();
            cts.Dispose();
            cts = null;
        }
    }
}

Now one can start an infinite action like InfiniteAction.Start() and stop it like InfiniteAction.Stop().

It could be done manually:

void ManualCancelation()
{
    var infiniteAction = new InfiniteAction(Rogue.RogueFunction);
    Console.WriteLine("RogueFunction is executing.");
    infiniteAction.Start();

    Console.WriteLine("Press any key to stop it.");
    Console.ReadKey();
    Console.WriteLine();
    infiniteAction.Stop();

    Console.WriteLine("Make sure it has stopped and press any key to exit.");
    Console.ReadKey();
    Console.WriteLine();
}

Or by timer:

void ByTimerCancelation()
{
    var interval = 3000;
    var infiniteAction = new InfiniteAction(Rogue.RogueFunction);
    Console.WriteLine($"RogueFunction is executing and will be stopped in {interval} ms.");
    Console.WriteLine("Make sure it has stopped and press any key to exit.");
    infiniteAction.Start();
    var timer = new Timer(StopInfiniteAction, infiniteAction, interval, -1);
    Console.ReadKey();
    Console.WriteLine();
}

private void StopInfiniteAction(object action)
{
    var infiniteAction = action as InfiniteAction;
    if (infiniteAction != null)
        infiniteAction.Stop();
    else
        throw new ArgumentException($"Invalid argument {nameof(action)}");
}
CSDev
  • 3,177
  • 6
  • 19
  • 37
  • Calling `Thread.Abort()` is unsafe. The only time it should ever be called is when you're trying to forcibly exit from the app. – Enigmativity Apr 28 '19 at 12:48
  • Code only answers are also frowned on unless the code is super obvious. I think yours needs some serious explanation. – Enigmativity Apr 28 '19 at 12:49
  • @Enigmativity, in this case it's a background thread encapsulated in a class, so it's not so evil for it's the only possible solution (here i agree with Theodor Zoulias's answer https://stackoverflow.com/a/55885754/10958092). What would you suggest? – CSDev Apr 28 '19 at 13:24
  • @Enigmativity, I broke the code into parts. Each should be pretty obvious. – CSDev Apr 28 '19 at 13:25
  • 1
    @Alex there is a race condition in your code. The `CancellationTokenSource` could be cancelled before the thread is started, causing the not-yet-started thread to abort, causing a `ThreadStartException`. You'd better switch the order of the commands `cts.Token.Register(t.Abort)` and `t.Start()`. Also you could keep a reference of the thread inside the `InfiniteAction` class, so that you can `Join` with the aborted thread after `cts.Cancel()`. – Theodor Zoulias Apr 28 '19 at 14:00
  • @Alex - Calling `Thread.Abort()` can corrupt the run-time causing the rest of your threads to not be reliable. It's not safe at all. – Enigmativity Apr 28 '19 at 14:06
  • @Enigmativity could you provide a link or code that demonstrates corruption of the run-time, after aborting a thread that alternates between writing in the `Console` and sleeping? – Theodor Zoulias Apr 28 '19 at 14:19
  • @Theodor Zoulias, good idea about race condition and exposing some functionality of the encapsulated thread. I think the class may be improved in many ways. – CSDev Apr 28 '19 at 16:00