Search code examples
c#task-parallel-librarycancellation-token

How to abort or terminate a task of TPL when cancellation token is unreachable?


Let's consider the method:

Task Foo(IEnumerable items, CancellationToken token)
{
    return Task.Run(() =>
    {
        foreach (var i in items)
            token.ThrowIfCancellationRequested();

    }, token);
}

Then I have a consumer:

var cts = new CancellationTokenSource();
var task = Foo(Items, cts.token);
task.Wait();

And the example of Items:

IEnumerable Items
{
    get
    {
        yield return 0;
        Task.Delay(Timeout.InfiniteTimeSpan).Wait();
        yield return 1;
    }
}

What about task.Wait? I cannot put my cancel token into collection of items.

How to kill the not responding task or get around this?


Solution

  • My previous solution was based on an optimistic assumption that the enumerable is likely to not hang and is quite fast. Thus we could sometimes sucrifice one thread of the system's thread pool? As Dax Fohl pointed out, the task will be still active even if its parent task has been killed by cancel exception. And in this regard, that could chock up the underlying ThreadPool, which is used by default task scheduler, if several collections have been frozen indefinitely.

    Consequently I have refactored ToCancellable method:

    public static IEnumerable<T> ToCancellable<T>(this IEnumerable<T> @this, CancellationToken token)
    {
        var enumerator = @this.GetEnumerator();
        var state = new State();
    
        for (; ; )
        {
            token.ThrowIfCancellationRequested();
    
            var thread = new Thread(s => { ((State)s).Result = enumerator.MoveNext(); }) { IsBackground = true, Priority = ThreadPriority.Lowest };
            thread.Start(state);
    
            try
            {
                while (!thread.Join(10))
                    token.ThrowIfCancellationRequested();
            }
            catch (OperationCanceledException)
            {
                thread.Abort();
                throw;
            }
    
            if (!state.Result)
                yield break;
    
            yield return enumerator.Current;
        }
    }
    

    And a helping class to manage the result:

    class State
    {
        public bool Result { get; set; }
    }
    

    It is safe to abort a detached thread.

    The pain, that I see here is a thread creation which is heavy. That could be solved by using custom thread pool along with producer-consumer pattern that will be able to handle abort exceptions in order to remove broken thread from the pool.

    Another problem is at Join line. What is the best pause here? Maybe that should be in user charge and shiped as a method argument.