10

I'm writing a library which includes scheduling functionality (not the standard TaskScheduler, IScheduler...) based on .Net Tasks. I'm using TaskCompletionSource, and Task.Status is critical for representing the status of underlying operations, including TaskStatus.Created, i.e. created but not yet started. I know returned Tasks should normally be hot, but for my manually controlled proxy Tasks, I really do want them initially as Created.

Unfortunately for me, the initial status of TaskCompletionSource.Task is WaitingForActivation, i.e. it's already gone past Created. Put differently, TaskCompletionSource supports two states, but I need three states:

Question: How can I get a Task that I can manually set to three different states? I.e. Task.Status can be set to:

1) Created
2) One of WaitingForActivation/WaitingForChildrenToComplete/WaitingToRun/Running
3) Either of RanToCompletion/Canceled/Faulted

The code below understandably complains about the type mismatch. I can instead wrap the Task by changing new Task<TResult> to new Task<Task<TResult>>, but to get back to Task<TResult> I have to Unwrap() it, and the unwrapped task will have status WaitingForActivation, bringing me back to square one.

I will have a large number of these, so blocking a thread with Wait() for each is not an option.

I have considered inheriting from Task and overriding members (using new), but if possible it would be nice to give the library user an actual Task instead of a DerivedTask, especially since I present regular Tasks to also be awaited in many other places.

Ideas?

private TaskCompletionSource<TResult> tcs;

private async Task<TResult> CreateStartCompleteAsync()
{
    await tcs.Task;
    if (tcs.Task.IsCanceled)
    {
        throw new OperationCanceledException("");
    }
    else if // etc.
}

public ColdTaskCompletionSource()
{
    tcs = new TaskCompletionSource<TResult>();
    Task = new Task<TResult>(() => CreateStartCompleteAsync());
}

Errors:
* Cannot convert lambda expression to delegate type 'System.Func' because some of the return types in the block are not implicitly convertible to the delegate return type
* Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'TResult'

Kristian Wedberg
  • 465
  • 4
  • 10
  • I think I've answered a closely related question [here](http://stackoverflow.com/a/22705236/1768303). – noseratio Feb 04 '15 at 11:50
  • 2
    I think if you want to have more control you should not base your API on `Task` but on a custom class that exposes exactly the information you want to expose. You can also expose a `Task` but the status info would come from your own class. – usr Feb 04 '15 at 11:55
  • @Noseratio: The `Task` in your linked code only goes through 2 states: `Created` and "Completed", similar to `TaskCompletionSource.Task` only going through 2 states (`WaitingForActivation` and "Completed"). I need **3** states (`Created`, `Running`, "Completed"), and my example code above attempts to use async-await to accomplish this, but as explained doesn't work due to type mismatch. I don't see how to modify your linked code to support **3** states - any ideas? – Kristian Wedberg Feb 04 '15 at 13:02
  • @usr: Yes I could create my own awaitable with custom status info, but I use regular (non-proxy) Tasks in many places, and want to present a single (standard i.e. Task) awaitable type to the library user. On the second suggestion: giving the user both a Task (which has Status) and my own separate status property would also be confusing and undesirable. Do let me know if I misunderstood anything. – Kristian Wedberg Feb 04 '15 at 13:14
  • You understood correctly. I do not want to derail the question but why do you *need* to return a Created task? What decisions are being made based on that status? Can't you just use Created || WaitingForActivation? – usr Feb 04 '15 at 13:27
  • Kristian, the task can only have `Running` state when it's actually executing something, i.e., holding a thread (that includes a blocking `Wait`). If the task is awaiting continuation (i.e. pending at the `await` points), its status is `WaitingForActivation`. Are you happy to sacrifice the scalability of your library for the sake of using `Running` instead of `WaitingForActivation`? – noseratio Feb 04 '15 at 13:42
  • @Noseratio: I will have a large number of these, so blocking a thread with Wait() for each is not an option, and hence my async-await attempt. I don't care if the second state is WaitingForActivation or WaitingToRun or..., just that it's separate from Created and from "Completed" states. – Kristian Wedberg Feb 04 '15 at 13:49
  • @usr: For my underlying operations there is logically a big difference between being "Not started" and "Started", and the library uses this status for other decisions. Being able to use `TaskStatus` directly would significantly reduce complexity and potential confusion. – Kristian Wedberg Feb 04 '15 at 15:00
  • 1
    @KristianWedberg I understand. I think its a misuse of the TPL to overload internal status codes with concepts only present in your scheduling library. I'd create a `class ScheduledTask { MyStatusEnum Status; Task Completed; }`. You can even make this type awaitable although I think that's an anti-pattern. Anyway, your question is valid and should be answered as asked. – usr Feb 04 '15 at 15:31
  • @usr TaskStatus codes are obviously part of the _public_ API, but I take your point, it e.g. wouldn't be inconceivable for new codes to be added. If instead TaskCompletionSource supported Created tasks directly, nobody would think twice about using it. I can feel a pull request coming when all of TPL is open sourced and available :-) – Kristian Wedberg Feb 05 '15 at 17:14

2 Answers2

6

While I agree with @usr's points in the comments, technically you still can have an implementation that provides these states:

  1. Created
  2. WaitingToRun
  3. Either RanToCompletion/Canceled/Faulted

To avoid blocking threads with Task.Wait, you could use an internal helper TaskScheduler, which would first transition the task from Created to WaitingToRun and, eventually, to one of the completed states.

The code below illustrates this concept. It was only very slightly tested, might be not fully thread-safe and perhaps could be improved to share a single instance of FakeTaskScheduler across multiple tasks.

public class ColdTaskCompletionSource
{
    public sealed class FakeTaskScheduler : TaskScheduler
    {
        Task _task;

        public FakeTaskScheduler()
        {
        }

        protected override void QueueTask(Task task)
        {
            _task = task;
        }

        protected sealed override bool TryDequeue(Task task)
        {
            if (task != _task)
                return false;

            _task = null;
            return true;
        }

        protected override IEnumerable<Task> GetScheduledTasks()
        {
            if (_task == null)
                yield break;
            yield return _task;
        }

        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            return false;
        }

        public override int MaximumConcurrencyLevel
        {
            get { return 1; }
        }

        public bool Execute()
        {
            if (_task == null)
                return false;

            var task = _task;
            _task = null;
            return base.TryExecuteTask(task);
        }
    }

    readonly Task _task;
    readonly CancellationTokenSource _cts;
    readonly object _lock = new Object();
    readonly FakeTaskScheduler _ts = new FakeTaskScheduler();
    Action _completionAction = null;

    // helpers

    void InvokeCompletionAction()
    {
        if (_completionAction != null)
            _completionAction();
    }

    void Complete()
    {
        if (_task.Status != TaskStatus.WaitingToRun)
            throw new InvalidOperationException("Invalid Task state");
        _ts.Execute();
    }

    // public API

    public ColdTaskCompletionSource()
    {
        _cts = new CancellationTokenSource();
        _task = new Task(InvokeCompletionAction, _cts.Token);
    }

    public Task Task { get { return _task; } }

    public void Start()
    {
        _task.Start(_ts);
    }

    public void SetCompleted()
    {
        lock (_lock)
            Complete();
    }

    public void SetException(Exception ex)
    {
        lock (_lock)
        {
            _completionAction = () => { throw ex; };
            Complete();
        }
    }

    public void SetCancelled()
    {
        lock (_lock)
        {
            _completionAction = () =>
            {
                _cts.Cancel();
                _cts.Token.ThrowIfCancellationRequested();
            };
            Complete();
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    @Noseratio: This worked very well, much appreciated! The above code _requires_ calling Start() as per my 'spec'. I revised it to make calling Start() optional by taking the lock also in Start(): `lock(_lock) _task.Start(_ts);`, and in Complete() instead of throwing I do: `if (_task.Status == TaskStatus.Created) _task.Start(_ts);` – Kristian Wedberg Feb 05 '15 at 16:54
2

You can't in a reasonable way.

The Task status is handled internally and the only API for creating tasks manually is using TaskCompletionSource. You also can't inherit from Task since the Status property is only a getter and you can't reach the backing field m_stateFlags since it's internal.

However, the unreasonable way would be to create a task that waits on a TaskCompletionSource and you control the task's status be controlling the TaskCompletionSource:

var taskCompletionSource = new TaskCompletionSource<bool>();
var cancellationTokenSource = new CancellationTokenSource();
var task = new Task(() => taskCompletionSource.Task.Wait(cancellationTokenSource.Token), cancellationTokenSource.Token); // task.Status == TaskStatus.Created

task.Start(); // task.Status == TaskStatus.Running

taskCompletionSource.SetResult(false) // task.Status == TaskStatus.RanToCompletion

Or

taskCompletionSource.SetException(new Exception("")) // task.Status == TaskStatus.Faulted

Or

cancellationTokenSource.Cancel() // task.Status == TaskStatus.Cancelled

I'm not really recommending you do that. I'm not sure why you want to control Task's status but you probably need to create your own construct you can manage yourself instead of forcing Task into a design it wasn't meant for.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • Why create a hot-running `Task` only to block a pool thread with `Task.Wait()`? It's quite [possible](http://stackoverflow.com/a/22705236) to simulate `TaskCompletionSource` with a cold `Task`. It might not be perfect either as it exposes the inner `Task` to the caller, but IMO still better than blocking. – noseratio Feb 04 '15 at 12:31
  • @Noseratio *"How can I get a Task that I manually set the Status (Created, **Running**, RanToCompletion, Canceled, Faulted)"* – i3arnon Feb 04 '15 at 12:59
  • @i3arnon: Edited the question - I have many of these and `Wait()` is not an option. – Kristian Wedberg Feb 04 '15 at 14:40