Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create ice cold TaskCompletionSource?

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'

like image 457
Kristian Wedberg Avatar asked Feb 04 '15 10:02

Kristian Wedberg


2 Answers

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();
        }
    }
}
like image 109
noseratio Avatar answered Nov 07 '22 01:11

noseratio


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.

like image 45
i3arnon Avatar answered Nov 07 '22 03:11

i3arnon